diff --git a/.gitignore b/.gitignore index 2cb145f..735bdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ .sass-cache/ -.idea/* +.idea/ +.DS_Store diff --git a/.idea/encodings.xml b/.idea/encodings.xml index d821048..088a572 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,4 +1,6 @@ - + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 0b7f347..21b367d 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,13 +2,9 @@ - - - - - - - + + + @@ -33,116 +29,101 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -173,43 +154,57 @@ @@ -218,7 +213,7 @@ @@ -244,16 +239,6 @@ - - - - - - - - @@ -310,91 +295,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -411,14 +316,6 @@ + + + - - - - - - - - @@ -572,14 +443,22 @@ - + + + + + + + + - + - - + + + @@ -593,14 +472,10 @@ - - - - - + @@ -610,6 +485,10 @@ + + + + @@ -687,15 +566,16 @@ - + + - + @@ -704,11 +584,10 @@ - - + @@ -739,411 +618,429 @@ - + - - + + - + - - + + - + - - - + + + + + - + - - - + + + + + - + - - + + - + - - - + + + + + - + - - - + + + + + - + - - + + - + - - - + + + + + - + - - + + + + + - + - - + + + - + - - + + + - + - - - + + + + + + + - + - - + + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + + - + - - + + + - + - - + + + - + - - + + - + - - + + + - + - - + + + + + - + - - + + + - + - - + + + + - + - - + + + - + - - + + - + - - + + + - + - - + + - + - - + + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - - - - - - - - - - - - - - - - - + + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + diff --git a/app/bower_components/es6-promise-polyfill/.bower.json b/app/bower_components/es6-promise-polyfill/.bower.json new file mode 100644 index 0000000..d33d023 --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/.bower.json @@ -0,0 +1,22 @@ +{ + "name": "es6-promise-polyfill", + "version": "1.0.0", + "main": "promise.js", + "ignore": [ + ".*", + "**/.*", + "node_modules", + "test" + ], + "homepage": "https://github.com/lahmatiy/es6-promise-polyfill", + "_release": "1.0.0", + "_resolution": { + "type": "version", + "tag": "v1.0.0", + "commit": "70c9b49bf318f47696065fd40bf6a54b41009d23" + }, + "_source": "git://github.com/lahmatiy/es6-promise-polyfill.git", + "_target": "~1.0.0", + "_originalSource": "es6-promise-polyfill", + "_direct": true +} \ No newline at end of file diff --git a/app/bower_components/es6-promise-polyfill/CHANGELOG.md b/app/bower_components/es6-promise-polyfill/CHANGELOG.md new file mode 100644 index 0000000..066a06b --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 (june 19, 2014) + +* Init release diff --git a/app/bower_components/es6-promise-polyfill/README.md b/app/bower_components/es6-promise-polyfill/README.md new file mode 100644 index 0000000..3c2d191 --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/README.md @@ -0,0 +1,65 @@ +# ES6 Promise polyfill + +This is a polyfill of [ES6 Promise](https://github.com/domenic/promises-unwrapping). The implementation based on [Jake Archibald implementation](https://github.com/jakearchibald/es6-promise) a subset of [rsvp.js](https://github.com/tildeio/rsvp.js). If you're wanting extra features and more debugging options, check out the [full library](https://github.com/tildeio/rsvp.js). + +For API details and how to use promises, see the JavaScript Promises HTML5Rocks article. + +## Notes + +The main target: implementation should be conformance with browser's implementations and to be minimal as possible in size. So it's strictly polyfill of ES6 Promise specification and nothing more. + +It passes both [Promises/A+ test suite](https://github.com/promises-aplus/promises-tests) and [rsvp.js test suite](https://github.com/jakearchibald/es6-promise/tree/master/test). And as small as 2,6KB min (or 1KB min+gzip). + +The polyfill uses `setImmediate` if available, or fallback to use `setTimeout`. Use [setImmediate polyfill](https://github.com/YuzuJS/setImmediate) by @YuzuJS to rich better performance. + +## How to use + +### Browser + +To install: + +```sh +bower install es6-promise-polyfill +``` + +To use: + +```htmpl + + +``` + +### Node.js + +To install: + +```sh +npm install es6-promise-polyfill +``` + +To use: + +```js +var Promise = require('es6-promise-polyfill').Promise; +var promise = new Promise(...); +``` + +## Usage in IE<9 + +`catch` is a reserved word in IE<9, meaning `promise.catch(func)` throws a syntax error. To work around this, use a string to access the property: + +```js +promise['catch'](function(err) { + // ... +}); +``` + +Or use `.then` instead: + +```js +promise.then(undefined, function(err) { + // ... +}); +``` diff --git a/app/bower_components/es6-promise-polyfill/bower.json b/app/bower_components/es6-promise-polyfill/bower.json new file mode 100644 index 0000000..8b28ad4 --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/bower.json @@ -0,0 +1,11 @@ +{ + "name": "es6-promise-polyfill", + "version": "1.0.0", + "main": "promise.js", + "ignore": [ + ".*", + "**/.*", + "node_modules", + "test" + ] +} \ No newline at end of file diff --git a/app/bower_components/es6-promise-polyfill/package.json b/app/bower_components/es6-promise-polyfill/package.json new file mode 100644 index 0000000..314af33 --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/package.json @@ -0,0 +1,27 @@ +{ + "name": "es6-promise-polyfill", + "namespace": "Promise", + "version": "1.0.0", + "author": "Roman Dvornov ", + "description": "A polyfill for ES6 Promise", + "main": "promise.js", + "directories": { + "lib": "lib" + }, + "devDependencies": { + }, + "scripts": { + }, + "repository": { + "type": "git", + "url": "git://github.com/lahmatiy/es6-promise-polyfill.git" + }, + "bugs": { + "url": "https://github.com/lahmatiy/es6-promise-polyfill/issues" + }, + "keywords": [ + "promises", + "futures", + "events" + ] +} diff --git a/app/bower_components/es6-promise-polyfill/promise.js b/app/bower_components/es6-promise-polyfill/promise.js new file mode 100644 index 0000000..c3f0295 --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/promise.js @@ -0,0 +1,326 @@ +(function(global){ + +// +// Check for native Promise and it has correct interface +// + +var NativePromise = global['Promise']; +var nativePromiseSupported = + NativePromise && + // Some of these methods are missing from + // Firefox/Chrome experimental implementations + 'resolve' in NativePromise && + 'reject' in NativePromise && + 'all' in NativePromise && + 'race' in NativePromise && + // Older version of the spec had a resolver object + // as the arg rather than a function + (function(){ + var resolve; + new NativePromise(function(r){ resolve = r; }); + return typeof resolve === 'function'; + })(); + + +// +// export if necessary +// + +if (typeof exports !== 'undefined' && exports) +{ + // node.js + exports.Promise = Promise || NativePromise; +} +else +{ + // in browser add to global + if (!nativePromiseSupported) + global['Promise'] = Promise; +} + + +// +// Polyfill +// + +var PENDING = 'pending'; +var SEALED = 'sealed'; +var FULFILLED = 'fulfilled'; +var REJECTED = 'rejected'; +var NOOP = function(){}; + +// async calls +var asyncSetTimer = typeof setImmediate !== 'undefined' ? setImmediate : setTimeout; +var asyncQueue = []; +var asyncTimer; + +function asyncFlush(){ + // run promise callbacks + for (var i = 0; i < asyncQueue.length; i++) + asyncQueue[i][0](asyncQueue[i][1]); + + // reset async asyncQueue + asyncQueue = []; + asyncTimer = false; +} + +function asyncCall(callback, arg){ + asyncQueue.push([callback, arg]); + + if (!asyncTimer) + { + asyncTimer = true; + asyncSetTimer(asyncFlush, 0); + } +} + + +function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } + + function rejectPromise(reason) { + reject(promise, reason); + } + + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } +} + +function invokeCallback(subscriber){ + var owner = subscriber.owner; + var settled = owner.state_; + var value = owner.data_; + var callback = subscriber[settled]; + var promise = subscriber.then; + + if (typeof callback === 'function') + { + settled = FULFILLED; + try { + value = callback(value); + } catch(e) { + reject(promise, e); + } + } + + if (!handleThenable(promise, value)) + { + if (settled === FULFILLED) + resolve(promise, value); + + if (settled === REJECTED) + reject(promise, value); + } +} + +function handleThenable(promise, value) { + var resolved; + + try { + if (promise === value) + throw new TypeError('A promises callback cannot return that same promise.'); + + if (value && (typeof value === 'function' || typeof value === 'object')) + { + var then = value.then; // then should be retrived only once + + if (typeof then === 'function') + { + then.call(value, function(val){ + if (!resolved) + { + resolved = true; + + if (value !== val) + resolve(promise, val); + else + fulfill(promise, val); + } + }, function(reason){ + if (!resolved) + { + resolved = true; + + reject(promise, reason); + } + }); + + return true; + } + } + } catch (e) { + if (!resolved) + reject(promise, e); + + return true; + } + + return false; +} + +function resolve(promise, value){ + if (promise === value || !handleThenable(promise, value)) + fulfill(promise, value); +} + +function fulfill(promise, value){ + if (promise.state_ === PENDING) + { + promise.state_ = SEALED; + promise.data_ = value; + + asyncCall(publishFulfillment, promise); + } +} + +function reject(promise, reason){ + if (promise.state_ === PENDING) + { + promise.state_ = SEALED; + promise.data_ = reason; + + asyncCall(publishRejection, promise); + } +} + +function publish(promise) { + promise.then_ = promise.then_.forEach(invokeCallback); +} + +function publishFulfillment(promise){ + promise.state_ = FULFILLED; + publish(promise); +} + +function publishRejection(promise){ + promise.state_ = REJECTED; + publish(promise); +} + +/** +* @class +*/ +function Promise(resolver){ + if (typeof resolver !== 'function') + throw new TypeError('Promise constructor takes a function argument'); + + if (this instanceof Promise === false) + throw new TypeError('Failed to construct \'Promise\': Please use the \'new\' operator, this object constructor cannot be called as a function.'); + + this.then_ = []; + + invokeResolver(resolver, this); +} + +Promise.prototype = { + constructor: Promise, + + state_: PENDING, + then_: null, + data_: undefined, + + then: function(onFulfillment, onRejection){ + var subscriber = { + owner: this, + then: new this.constructor(NOOP), + fulfilled: onFulfillment, + rejected: onRejection + }; + + if (this.state_ === FULFILLED || this.state_ === REJECTED) + { + // already resolved, call callback async + asyncCall(invokeCallback, subscriber); + } + else + { + // subscribe + this.then_.push(subscriber); + } + + return subscriber.then; + }, + + 'catch': function(onRejection) { + return this.then(null, onRejection); + } +}; + +Promise.all = function(promises){ + var Class = this; + + if (!Array.isArray(promises)) + throw new TypeError('You must pass an array to Promise.all().'); + + return new Class(function(resolve, reject){ + var results = []; + var remaining = 0; + + function resolver(index){ + remaining++; + return function(value){ + results[index] = value; + if (!--remaining) + resolve(results); + }; + } + + for (var i = 0, promise; i < promises.length; i++) + { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') + promise.then(resolver(i), reject); + else + results[i] = promise; + } + + if (!remaining) + resolve(results); + }); +}; + +Promise.race = function(promises){ + var Class = this; + + if (!Array.isArray(promises)) + throw new TypeError('You must pass an array to Promise.race().'); + + return new Class(function(resolve, reject) { + for (var i = 0, promise; i < promises.length; i++) + { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') + promise.then(resolve, reject); + else + resolve(promise); + } + }); +}; + +Promise.resolve = function(value){ + var Class = this; + + if (value && typeof value === 'object' && value.constructor === Class) + return value; + + return new Class(function(resolve){ + resolve(value); + }); +}; + +Promise.reject = function(reason){ + var Class = this; + + return new Class(function(resolve, reject){ + reject(reason); + }); +}; + +})(new Function('return this')()); diff --git a/app/bower_components/es6-promise-polyfill/promise.min.js b/app/bower_components/es6-promise-polyfill/promise.min.js new file mode 100644 index 0000000..ee30fcc --- /dev/null +++ b/app/bower_components/es6-promise-polyfill/promise.min.js @@ -0,0 +1,5 @@ +(function(t){function x(){for(var a=0;a= 1.9.0", "_originalSource": "jquery" } \ No newline at end of file diff --git a/app/icons/.fontcustom-manifest.json b/app/icons/.fontcustom-manifest.json new file mode 100644 index 0000000..822efd2 --- /dev/null +++ b/app/icons/.fontcustom-manifest.json @@ -0,0 +1,83 @@ +{ + "checksum": { + "previous": "9d31e57057239d5c84c067dc119bc27f52680588b4c561ca461fb16e2b4fbfcf", + "current": "9d31e57057239d5c84c067dc119bc27f52680588b4c561ca461fb16e2b4fbfcf" + }, + "fonts": [ + "../styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.ttf", + "../styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.svg", + "../styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.woff", + "../styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.eot" + ], + "glyphs": { + "circle": { + "codepoint": 61704, + "source": "svgs/circle.svg" + }, + "line-pause": { + "codepoint": 61696, + "source": "svgs/line-pause.svg" + }, + "line-play": { + "codepoint": 61697, + "source": "svgs/line-play.svg" + }, + "line-radio-microphone": { + "codepoint": 61698, + "source": "svgs/line-radio-microphone.svg" + }, + "line-stop": { + "codepoint": 61699, + "source": "svgs/line-stop.svg" + }, + "pause": { + "codepoint": 61700, + "source": "svgs/pause.svg" + }, + "play": { + "codepoint": 61701, + "source": "svgs/play.svg" + }, + "radio-microphone": { + "codepoint": 61702, + "source": "svgs/radio-microphone.svg" + }, + "stop": { + "codepoint": 61703, + "source": "svgs/stop.svg" + } + }, + "options": { + "autowidth": false, + "config": "fontcustom.yml", + "css_selector": ".icon-{{glyph}}", + "debug": false, + "font_ascent": 448, + "font_descent": 64, + "font_design_size": 16, + "font_em": 512, + "font_name": "fontcustom", + "fonts": "../styles/fonts", + "force": false, + "input": { + "templates": "svgs/", + "vectors": "svgs/" + }, + "no_hash": false, + "output": { + "css": "../styles/fonts", + "fonts": "../styles/fonts", + "preview": "../styles/fonts" + }, + "preprocessor_path": "./", + "quiet": false, + "templates": [ + "scss", + "preview" + ] + }, + "templates": [ + "../styles/fonts/_fontcustom.scss", + "../styles/fonts/fontcustom-preview.html" + ] +} \ No newline at end of file diff --git a/app/icons/acknowledgment.txt b/app/icons/acknowledgment.txt new file mode 100644 index 0000000..6a7ff8b --- /dev/null +++ b/app/icons/acknowledgment.txt @@ -0,0 +1,12 @@ +ICONS +radio-microphone by Travis Yunis, Campinas, São Paulo, BR 2012 +line-radio-microphone by Tim Boelaars, Amsterdam, Noord-Holland, NL 2013 + +play by Convoy, Hamburg, Hamburg, DE 2012 +line-play by Luboš Volkov, 2013 + +stop by Mateo Zlatar, Brooklyn, New York, US 2012 +line-stop by artworkbean + +line-pause by Austin Condiff, South Jordan, Utah, US 2014 +pause by Chris Thoburn, Chicago 2012 diff --git a/app/icons/fontcustom.yml b/app/icons/fontcustom.yml new file mode 100644 index 0000000..452d84f --- /dev/null +++ b/app/icons/fontcustom.yml @@ -0,0 +1,95 @@ +# ============================================================================= +# Font Custom Configuration +# This file should live in the directory where you run `fontcustom compile`. +# For more info, visit . +# ============================================================================= + + +# ----------------------------------------------------------------------------- +# Project Info +# ----------------------------------------------------------------------------- + +# The font's name. Also determines the file names of generated templates. +#font_name: icons + +# Format of CSS selectors. {{glyph}} is substituted for the glyph name. +#css_selector: .i-{{glyph}} + +# Generate fonts without asset-busting hashes. +#no_hash: true + +# Encode WOFF fonts into the generated CSS. +#base64: true + +# Forces compilation, even if inputs have not changed +#force: true + +# Display (possibly useful) debugging messages. +#debug: true + +# Hide status messages. +#quiet: true + + +# ----------------------------------------------------------------------------- +# Input / Output Locations +# You can save generated fonts, CSS, and other files to different locations +# here. Font Custom can also read input vectors and templates from different +# places. +# +# NOTE: +# - Be sure to preserve the whitespace in these YAML hashes. +# - INPUT[:vectors] and OUTPUT[:fonts] are required. Everything else is +# optional. +# - Specify output locations for custom templates by including their file +# names as the key. +# ----------------------------------------------------------------------------- + +input: svgs/ +#vectors: ./svgs/ +#templates: my/templates + +output: ../styles/fonts +fonts: ../styles/fonts +#css: ./app/styles/fonts +# my-custom-template.yml: path/to/template/output + + +# ----------------------------------------------------------------------------- +# Templates +# A YAML array of templates and files to generate alongside fonts. Custom +# templates should be saved in the INPUT[:templates] directory and referenced +# by their base file name. +# +# For Rails and Compass templates, set `preprocessor_path` as the relative +# path from OUTPUT[:css] to OUTPUT[:fonts]. By default, these are the same +# directory. +# +# Included in Font Custom: preview, css, scss, scss-rails +# Default: css, preview +# ----------------------------------------------------------------------------- + +templates: +- scss +- preview +#- my-custom-template.yml + +preprocessor_path: ./ + + +# ----------------------------------------------------------------------------- +# Font Settings (defaults shown) +# ----------------------------------------------------------------------------- + +# Size (in pica points) for which your font is designed. +#font_design_size: 16 + +# The em size. Setting this will scale the entire font to the given size. +#font_em: 512 + +# The font's ascent and descent. Used to calculate the baseline. +#font_ascent: 448 +#font_descent: 64 + +# Horizontally fit glyphs to their individual vector widths. +#autowidth: false diff --git a/app/icons/svgs/.fontcustom-manifest.json b/app/icons/svgs/.fontcustom-manifest.json new file mode 100644 index 0000000..1422c85 --- /dev/null +++ b/app/icons/svgs/.fontcustom-manifest.json @@ -0,0 +1,78 @@ +{ + "checksum": { + "previous": "9d23709f74b077ae6d8aed54c2654b736a5825fa302a951d6b36ee9da006af92", + "current": "9d23709f74b077ae6d8aed54c2654b736a5825fa302a951d6b36ee9da006af92" + }, + "fonts": [ + "fontcustom/fontcustom_9d23709f74b077ae6d8aed54c2654b73.ttf", + "fontcustom/fontcustom_9d23709f74b077ae6d8aed54c2654b73.svg", + "fontcustom/fontcustom_9d23709f74b077ae6d8aed54c2654b73.woff", + "fontcustom/fontcustom_9d23709f74b077ae6d8aed54c2654b73.eot" + ], + "glyphs": { + "line-pause": { + "codepoint": 61696, + "source": "./line-pause.svg" + }, + "line-play": { + "codepoint": 61697, + "source": "./line-play.svg" + }, + "line-radio-microphone": { + "codepoint": 61698, + "source": "./line-radio-microphone.svg" + }, + "line-stop": { + "codepoint": 61699, + "source": "./line-stop.svg" + }, + "pause": { + "codepoint": 61700, + "source": "./pause.svg" + }, + "play": { + "codepoint": 61701, + "source": "./play.svg" + }, + "radio-microphone": { + "codepoint": 61702, + "source": "./radio-microphone.svg" + }, + "stop": { + "codepoint": 61703, + "source": "./stop.svg" + } + }, + "options": { + "autowidth": false, + "config": false, + "css_selector": ".icon-{{glyph}}", + "debug": false, + "font_ascent": 448, + "font_descent": 64, + "font_design_size": 16, + "font_em": 512, + "font_name": "fontcustom", + "force": false, + "input": { + "templates": ".", + "vectors": "." + }, + "no_hash": false, + "output": { + "css": "fontcustom", + "fonts": "fontcustom", + "preview": "fontcustom" + }, + "preprocessor_path": null, + "quiet": false, + "templates": [ + "css", + "preview" + ] + }, + "templates": [ + "fontcustom/fontcustom.css", + "fontcustom/fontcustom-preview.html" + ] +} \ No newline at end of file diff --git a/app/icons/svgs/circle.svg b/app/icons/svgs/circle.svg new file mode 100644 index 0000000..2babca7 --- /dev/null +++ b/app/icons/svgs/circle.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/icons/svgs/line-pause.svg b/app/icons/svgs/line-pause.svg new file mode 100644 index 0000000..b97b793 --- /dev/null +++ b/app/icons/svgs/line-pause.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/icons/svgs/line-play.svg b/app/icons/svgs/line-play.svg new file mode 100644 index 0000000..4ac9c93 --- /dev/null +++ b/app/icons/svgs/line-play.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/icons/svgs/line-radio-microphone.svg b/app/icons/svgs/line-radio-microphone.svg new file mode 100644 index 0000000..e5177f7 --- /dev/null +++ b/app/icons/svgs/line-radio-microphone.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/app/icons/svgs/line-stop.svg b/app/icons/svgs/line-stop.svg new file mode 100644 index 0000000..98d573a --- /dev/null +++ b/app/icons/svgs/line-stop.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/icons/svgs/pause.svg b/app/icons/svgs/pause.svg new file mode 100644 index 0000000..8daa8db --- /dev/null +++ b/app/icons/svgs/pause.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/icons/svgs/play.svg b/app/icons/svgs/play.svg new file mode 100644 index 0000000..6f0cd4d --- /dev/null +++ b/app/icons/svgs/play.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/icons/svgs/radio-microphone.svg b/app/icons/svgs/radio-microphone.svg new file mode 100644 index 0000000..4834e91 --- /dev/null +++ b/app/icons/svgs/radio-microphone.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/app/icons/svgs/stop.svg b/app/icons/svgs/stop.svg new file mode 100644 index 0000000..8d0a992 --- /dev/null +++ b/app/icons/svgs/stop.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/scripts/actions/fc-actions.js b/app/scripts/actions/fc-actions.js index 1983271..7a7f49a 100644 --- a/app/scripts/actions/fc-actions.js +++ b/app/scripts/actions/fc-actions.js @@ -2,26 +2,201 @@ var FCConstants = require('../constants/fc-constants'); var FCDispatcher = require('../dispatcher/fc-dispatcher'); var FCActions = { + + /** + * Register a reference to the audio object + * @param {object} audio + */ + registerAudio: function( audio ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.REGISTER_AUDIO, + audio: audio + }); + }, + + /** + * Recorder not available for this user or browser + */ + recorderNotAllowable: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.RECORDER_NOT_ALLOWABLE + }); + }, + + /** + * Register a reference to the recording object + * @param {object} recorder + */ + registerRecorder: function( recorder ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.REGISTER_RECORDER, + recorder: recorder + }); + }, + /** - * Stop recording the video + * Starts the recording the events on the code editor */ - stopRecording: function(){ + startRecordingVideo: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.START_RECORDING_VIDEO + }); + }, + + /** + * Starts the recording the audio for the screencast + */ + startRecordingAudio: function(){ FCDispatcher.handleViewAction({ - actionType: FCConstants.STOP_RECORDING + actionType: FCConstants.START_RECORDING_AUDIO }); }, /** - * Creates a playable video format that can be played by application - * @param {eventLog} The events that have been recorded and logged. - * @return {video} The index of the callback within the _callbacks array. + * Lifecycle event to have any changes occur after the start of the recording */ - createVideoFormat: function( eventLog ){ + startMainPhaseRecording: function(){ FCDispatcher.handleViewAction({ - actionType: FCConstants.CREATE_VIDEO_FORMAT, - eventLog: eventLog + actionType: FCConstants.MAIN_PHASE_RECORDING }); + }, + + + captureCodingEvent: function( evt ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.CODING_EVENT, + event: evt + }); + }, + + /** + * Stop recording the video + */ + stopRecordingVideo: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.STOP_RECORDING_VIDEO + }); + }, + + /** + * Handles stop recording audio + */ + stopRecordingAudio: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.STOP_RECORDING_AUDIO + }); + }, + + /** + * Registers the audio file that was generated by recorder + * @param audioFile + */ + registerAudioFile: function(audioFile){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.REGISTER_AUDIO_FILE, + audioFile: audioFile + }) + }, + + /** + * Play back the audio file + */ + playbackAudio: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PLAYBACK_AUDIO + }); + }, + + /** + * Play back the video file + */ + playbackVideo: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PLAYBACK_VIDEO + }); + }, + + /** + * Registers the start time of the video + * @param startTime - The moment the video starts + */ + registerPlaybackStartTime: function( startTime ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.REGISTER_PLAYBACK_START_TIME, + playbackStartTime: startTime + }) + }, + + /** + * Registers the interval id of the event loop running for key events + * @param {Array} playbackIntervalIds + */ + registerPlaybackIntervalIds: function( playbackIntervalIds ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.REGISTER_PLAYBACK_INTERVAL_IDS, + playbackIntervalIds: playbackIntervalIds + }); + }, + + /** + * Pause the playing audio + */ + pauseAudio: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PAUSE_AUDIO + }); + }, + + /** + * Pause the playing video + */ + pauseVideo: function(){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PAUSE_VIDEO + }); + }, + + /** + * Registers the current video text for synchronization purposes + * @param videoText + */ + capturePausedVideoState: function( videoText ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PAUSED_VIDEO_STATE, + videoText: videoText + }); + }, + + /** + * Registers the current video time when paused for synchronizaiton purposes + * @param currentVideoTime + */ + capturePausedVideoTime: function( currentVideoTime ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.PAUSED_VIDEO_TIME, + currentVideoTime: currentVideoTime + }); + }, + + setCurrentTick: function( tick ){ + FCDispatcher.handleViewAction({ + actionType: FCConstants.CURRENT_TICK, + currentTick: tick + }) } + + // + ///** + // * Creates a playable video format that can be played by application + // * @param {eventLog} The events that have been recorded and logged. + // * @return {video} The index of the callback within the _callbacks array. + // */ + //createVideoFormat: function( eventLog ){ + // FCDispatcher.handleViewAction({ + // actionType: FCConstants.CREATE_VIDEO_FORMAT, + // eventLog: eventLog + // }); + //} }; module.exports = FCActions; diff --git a/app/scripts/app.js b/app/scripts/app.js index 6650a84..fe09017 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -1,10 +1,12 @@ /** @jsx React.DOM */ +// Entire super set of application for client side rendering var React = require('react/addons'); var FollowApp = React.createFactory(require('./components/FollowApp').FollowApp); var mountNode = document.getElementById('app'); +var Audio = require('./async/audio'); -// Async Components +// Async Components not safe for server side var AsyncCodePlayground = require('./components/code-playground'); // Client side rendering on top of server rendering diff --git a/app/scripts/async/_recorder.js b/app/scripts/async/_recorder.js new file mode 100644 index 0000000..0c77292 --- /dev/null +++ b/app/scripts/async/_recorder.js @@ -0,0 +1,124 @@ +/*License (MIT) + + Copyright © 2013 Matt Diamond + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of + the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + */ + +var WORKER_PATH = '/scripts/async/_recorderWorker.js'; + +var Recorder = function(source, cfg){ + var config = cfg || {}; + var bufferLen = config.bufferLen || 4096; + this.context = source.context; + if(!this.context.createScriptProcessor){ + this.node = this.context.createJavaScriptNode(bufferLen, 2, 2); + } else { + this.node = this.context.createScriptProcessor(bufferLen, 2, 2); + } + + var worker = new Worker(config.workerPath || WORKER_PATH); + worker.postMessage({ + command: 'init', + config: { + sampleRate: this.context.sampleRate + } + }); + var recording = false, + currCallback; + + this.node.onaudioprocess = function(e){ + if (!recording) return; + worker.postMessage({ + command: 'record', + buffer: [ + e.inputBuffer.getChannelData(0), + e.inputBuffer.getChannelData(1) + ] + }); + }; + + this.configure = function(cfg){ + for (var prop in cfg){ + if (cfg.hasOwnProperty(prop)){ + config[prop] = cfg[prop]; + } + } + }; + + this.record = function(){ + recording = true; + }; + + this.stop = function(){ + recording = false; + }; + + this.clear = function(){ + worker.postMessage({ command: 'clear' }); + }; + + this.getBuffers = function(cb) { + currCallback = cb || config.callback; + worker.postMessage({ command: 'getBuffers' }) + }; + + this.exportWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportWAV', + type: type + }); + }; + + this.exportMonoWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportMonoWAV', + type: type + }); + }; + + worker.onmessage = function(e){ + console.log(e); + var blob = e.data; + currCallback(blob); + }; + + source.connect(this.node); + this.node.connect(this.context.destination); // if the script node is not connected to an output the "onaudioprocess" event is not triggered in chrome. +}; + +/** + * Store an audio file and return a reference to this file + * @param {object} blob - the raw audio + * @param {string} filename - optional parameter for the name of the file + * @returns {string} fileName - url for the source of the audio + */ +Recorder.prototype.setupDownload = function(blob, filename){ + var url = (window.URL || window.webkitURL).createObjectURL(blob); + var download = filename || 'output.wav'; + + // todo: create a link with the url created + console.log(url); + console.log(download); + return url; +}; + +module.exports = Recorder; diff --git a/app/scripts/async/_recorderWorker.js b/app/scripts/async/_recorderWorker.js new file mode 100644 index 0000000..dce4442 --- /dev/null +++ b/app/scripts/async/_recorderWorker.js @@ -0,0 +1,161 @@ +/*License (MIT) + + Copyright © 2013 Matt Diamond + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of + the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + */ + +var recLength = 0, + recBuffersL = [], + recBuffersR = [], + sampleRate; + +this.onmessage = function(e){ + switch(e.data.command){ + case 'init': + init(e.data.config); + break; + case 'record': + record(e.data.buffer); + break; + case 'exportWAV': + exportWAV(e.data.type); + break; + case 'exportMonoWAV': + exportMonoWAV(e.data.type); + break; + case 'getBuffers': + getBuffers(); + break; + case 'clear': + clear(); + break; + } +}; + +function init(config){ + sampleRate = config.sampleRate; +} + +function record(inputBuffer){ + recBuffersL.push(inputBuffer[0]); + recBuffersR.push(inputBuffer[1]); + recLength += inputBuffer[0].length; +} + +function exportWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var bufferR = mergeBuffers(recBuffersR, recLength); + var interleaved = interleave(bufferL, bufferR); + var dataview = encodeWAV(interleaved); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function exportMonoWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var dataview = encodeWAV(bufferL, true); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function getBuffers() { + var buffers = []; + buffers.push( mergeBuffers(recBuffersL, recLength) ); + buffers.push( mergeBuffers(recBuffersR, recLength) ); + this.postMessage(buffers); +} + +function clear(){ + recLength = 0; + recBuffersL = []; + recBuffersR = []; +} + +function mergeBuffers(recBuffers, recLength){ + var result = new Float32Array(recLength); + var offset = 0; + for (var i = 0; i < recBuffers.length; i++){ + result.set(recBuffers[i], offset); + offset += recBuffers[i].length; + } + return result; +} + +function interleave(inputL, inputR){ + var length = inputL.length + inputR.length; + var result = new Float32Array(length); + + var index = 0, + inputIndex = 0; + + while (index < length){ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; + } + return result; +} + +function floatTo16BitPCM(output, offset, input){ + for (var i = 0; i < input.length; i++, offset+=2){ + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeString(view, offset, string){ + for (var i = 0; i < string.length; i++){ + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function encodeWAV(samples, mono){ + var buffer = new ArrayBuffer(44 + samples.length * 2); + var view = new DataView(buffer); + + /* RIFF identifier */ + writeString(view, 0, 'RIFF'); + /* file length */ + view.setUint32(4, 32 + samples.length * 2, true); + /* RIFF type */ + writeString(view, 8, 'WAVE'); + /* format chunk identifier */ + writeString(view, 12, 'fmt '); + /* format chunk length */ + view.setUint32(16, 16, true); + /* sample format (raw) */ + view.setUint16(20, 1, true); + /* channel count */ + view.setUint16(22, mono?1:2, true); + /* sample rate */ + view.setUint32(24, sampleRate, true); + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * 4, true); + /* block align (channel count * bytes per sample) */ + view.setUint16(32, 4, true); + /* bits per sample */ + view.setUint16(34, 16, true); + /* data chunk identifier */ + writeString(view, 36, 'data'); + /* data chunk length */ + view.setUint32(40, samples.length * 2, true); + + floatTo16BitPCM(view, 44, samples); + + return view; +} diff --git a/app/scripts/async/audio.js b/app/scripts/async/audio.js new file mode 100644 index 0000000..7598559 --- /dev/null +++ b/app/scripts/async/audio.js @@ -0,0 +1,174 @@ +/** + * Create and Ship Audio object + * It spawns a web worker thread that it delegates + */ + +// --- Dependencies --- // +var Recorder = require('./_recorder'); +var FCActions = require('../actions/fc-actions'); + +// --- Audio Model Object --- // +var _audio = { + audioContext: null, + sound: null, + audioSource: null +}; + +var _recorder = {}; + + + +// --- Audio Component Factories --- // +/** + * Creates an AudioContext interface graph built from audio modules linked together + * @return {object} AudioContext + */ +function createAudioContext(){ + window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; + return new AudioContext(); +} + +/** + * Creates a GainNode object representing an interface for change in volume + * @param {object} audioContext (type: AudioContext) + * @return {object} GainNode + */ +function createGainNode( audioContext ){ + return audioContext.createGain(); +} + +/** + * Creates a MediaStreamAudioSourceNode object representing the audio source of the stream + * @param {object, object} audioContext and stream (type: AudioContext, type: MediaStream) + * @return {object} MediaStreamAudioSourceNode + */ +function createMediaStreamSource( audioContext, stream ){ + return audioContext.createMediaStreamSource(stream); +} + +/** + * Creates a Recorder object which spawns a background worker to record our audio output sound + * @param {object} sound (type: GainNode) + * @return {object} Recorder + */ +function createRecorder( sound ){ + return new Recorder(_audio.sound); +} +// --- End Audio Component Factories --- // + + + +//function updateAnalysers(time) { +// var analyserContext = null; +// if (!analyserContext) { +// var canvas = document.getElementById("analyser"); +// canvasWidth = canvas.width; +// canvasHeight = canvas.height; +// analyserContext = canvas.getContext('2d'); +// } +// +// var SPACING = 12; +// var BAR_WIDTH = 12; +// var numBars = Math.round(canvasWidth / SPACING); +// var freqByteData = new Uint8Array(analyserNode.frequencyBinCount); +// +// analyserNode.getByteFrequencyData(freqByteData); +// +// analyserContext.clearRect(0, 0, canvasWidth, canvasHeight); +// analyserContext.fillStyle = '#912590'; +// analyserContext.lineCap = 'round'; +// var multiplier = analyserNode.frequencyBinCount / numBars; +// var step = Math.round(freqByteData.length / numBars); +// // Draw rectangle for each frequency bin. +// for (var i = 0; i < numBars; ++i) { +// +// var value = freqByteData[i * step]; +// var magnitude = 0; +// var offset = Math.floor( i * multiplier ); +// for (var j = 0; j< multiplier; j++) +// magnitude += freqByteData[offset + j]; +// magnitude = magnitude / multiplier; +// analyserContext.fillRect(i * (SPACING + 4), canvasHeight - (value / 10), BAR_WIDTH, 40); +// } +// +// +// var rafId = window.requestAnimationFrame( updateAnalysers ); +// FCActions.registerVisualizerAnimationId(rafId); +//} + +// --- Initializations --- // +/** + * Mutable Function that initializes our audio components from a successful callback from MediaStream creation + * @param {object} stream (type: MediaStream) object that will be used to initialize our audio object and components + */ +function initAudioComponents( stream ) { + // Create a generic audio context object + _audio.audioContext = createAudioContext(); + // Create a volume object representing the sound + _audio.sound = createGainNode(_audio.audioContext); + // Create a audioSource from our stream + _audio.audioSource = createMediaStreamSource(_audio.audioContext, stream); + + // Connect the audioSource node input node with the gainNode sound output + _audio.audioSource.connect(_audio.sound); + + // Creates Recorder object + _recorder = createRecorder(_audio.sound); + + // Convert to Mono? + //audioInput = convertToMono( input ); + + // Visual depiction options of Audio context (Needed?) + // CALL THE VISUALIZER HER PASSING IN THE AUDIOCONTEXT and + //var visualizer = new Visualizer; + //visualizer.visualize(_audio.audioContext, _audio.audioContext.createBuffer(2, 22050, 44100)); + + //var analyserNode = _audio.audioContext.createAnalyser(); + //analyserNode.fftSize = 2048; + //_audio.sound.connect( analyserNode ); + // + //var zeroGain = _audio.audioContext.createGain(); + //zeroGain.gain.value = 0.0; + //_audio.sound.connect( zeroGain ); + //zeroGain.connect( _audio.audioContext.destination ); + //updateAnalysers(); + + // Register our newly created audio and recorder + FCActions.registerAudio(_audio); + FCActions.registerRecorder(_recorder); +} + +function initAudioStream() { + // Determine and set browser related navigator media objects + if (!navigator.getUserMedia) + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + if (!navigator.cancelAnimationFrame) + navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame; + if (!navigator.requestAnimationFrame) + navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame; + + // Request that a user allows mic input recording with some options + navigator.getUserMedia( + { + "audio": { + "mandatory": { + "googEchoCancellation": "false", + "googAutoGainControl": "false", + "googNoiseSuppression": "false", + "googHighpassFilter": "false" + }, + "optional": [] + } + }, initAudioComponents, function(e) { + // todo: Handle error / non-success + FCActions.recorderNotAllowable(); + console.log('Can not record without audio input'); + console.log(e); + }); +} + +// Initialize Audio Stream on Window's load event +window.addEventListener('load', initAudioStream ); + + +module.exports = _audio; diff --git a/app/scripts/components/FollowApp.js b/app/scripts/components/FollowApp.js index e1d157f..f4ced7f 100644 --- a/app/scripts/components/FollowApp.js +++ b/app/scripts/components/FollowApp.js @@ -1,130 +1,72 @@ /** @jsx React.DOM */ +// This file contains only app components that are safe to render on server side var React = require('react/addons'); -var EndRecording = require('./end-recording'); -function Video( startTime ){ - //this.startState = startState; - this.startTime = startTime; - this.events = {}; - - return this; -} +// Top Level Components +var RecordingActions = require('./recording_actions/recording-actions'); +var Player = require('./player/player'); var FollowApp = React.createClass({ getInitialState: function(){ return { - video: {}, + audio: {}, videoEvents: {}, eventQueue: {}, editor: {}, - recording: false, + recording: false }; }, - - //*** Video Functions ***// - _processEvents: function(){ - var self = this; - // *Takes events in eventQueue and creates a playable video - var TICKINCREMENT = 1000; - var totalTicks = Math.ceil(this.state.video.lastEventTime / TICKINCREMENT); - var eventQueue = this.state.eventQueue; - - // for each tick, determine how many events occurred - for (var tick = 0; tick < totalTicks; tick++){ - var startOfTick = tick * TICKINCREMENT; - var endOfTick = startOfTick + TICKINCREMENT; - - // Iterate over events - var mskeys = Object.keys(eventQueue); - mskeys.forEach(function(eventTime){ - // loop over ms eventTimes checking for matches within tick range - if (eventTime >= startOfTick && eventTime < endOfTick){ - // {0: [{relativeTime: evt}, {relativeTime: evt}] } - // {1: [{relativeTime: evt}] } - // evenTime should be in ms relativeTime to tick - - var relativeTime = eventTime - startOfTick; - - if (tick in self.state.videoEvents){ // push event if tick is already present - // place event within tick with new relative time - self.state.videoEvents[tick][relativeTime] = self.state.eventQueue[eventTime]; - } else { // create tick with first event - // create object first - self.state.videoEvents[tick] = {}; - self.state.videoEvents[tick][relativeTime] = self.state.eventQueue[eventTime]; - } - } - }) - } - - console.log('this.state.videoEvents'); - console.log(this.state.videoEvents); - - console.log('this.state.eventQueue'); - console.log(this.state.eventQueue); - - }, - - //*** App Functions ***// - updateEditor: function( evt ){ - // Don't rerender the application, silently update state - if (this.state.recording){ - // Calculate event time slot - var eventTime = Date.now(), - eventIndex = Math.floor((eventTime - this.state.video.startTime)); - - this.state.eventQueue[eventIndex] = evt.data; - this.state.video.lastEventTime = eventIndex; - } - }, - startRecording: function(){ - if (!this.state.recording){ - this.setState({ recording: true}); - this.setState({ video: new Video(Date.now()) }); - console.log(this.state.video); - } - }, - endRecording: function(){ - if (this.state.recording){ - this.setState({ recording: false}); - this._processEvents(); - } - }, + /** + * Register the editor locally so children can reference it + * @param editor + */ registerEditor: function( editor ){ - this.state.editor = editor; + this.setState({editor: editor}); }, - replayVideo: function(){ - console.log('playing') - var videoEvents = this.state.videoEvents; - var TICKINCREMENT = 1000; - var document = this.state.editor.getSession().getDocument(); - - var tick = 0; - var playbackId = setInterval(function(){ // Every tick interval, set up queue events to fire - - console.log('tick: ' + tick); - var eventsForTickArr = videoEvents[tick]; - if (eventsForTickArr){ // If we have events for this tick - var mskeys = Object.keys(eventsForTickArr); - console.log(mskeys); - mskeys.forEach(function(eventTime, index){ - setTimeout(function(){ - document.applyDeltas([ eventsForTickArr[eventTime] ]); - }, eventTime); - }); - } - - tick++; - }, TICKINCREMENT); - - setTimeout(function(){ - // clear playback after finished - console.log('Video Done'); - clearInterval(playbackId); - }, this.state.video.lastEventTime + 1000); - }, + //shouldComponentUpdate: function(nextProps, nextState){ + // // Unfortunately for our top level component, every rerender causes our 3rd party code editor + // // to lose it state. It's state is not complete under our control. + // // This necessitates only re-rendering when we want to replay our video. + // + // // Check previous isPlaying value and compare it to nextState + // if (nextState.audio.isPlaying === true && isPlaying === false){ + // //// start playing back of audio + // //var audioTag = this.refs.audioPlayer.getDOMNode(); + // //console.log('audioTag->', audioTag); + // + // } + // if (nextState.video.isPlaying === true && isPlaying === false) { + // // start playing back of audio and reflect isPlaying change + // isPlaying = true; + // return true; + // } + // return false; + // // todo: *cleanup* + // //console.log('recognizing a change in the top level element'); + // //// check cached value to see if there was a change + // //console.log('isPlaying->', isPlaying); + // //console.log('current Video state->', this.state.video); + // //if (isPlaying === false && this.state.video === true){ + // // console.log('Going for the replay'); + // // isPlaying = true; + // // _replayVideo(); + // //} + // + //}, + + //_onChange: function() { + // console.log('setting audio state'); + // this.setState({audio: this._getAudio()}); + //}, + + + + /** + * Renders our Follow Codes application + * @returns {XML} + */ render: function() { // async loading of codePlayground // todo: Come up with a better solution to do this @@ -135,15 +77,11 @@ var FollowApp = React.createClass({ return (
{AsyncCodePlayground({updateEditor: this.updateEditor, registerEditorState: this.registerEditor})} - - - - + +
); } }); /* Module.exports instead of normal dom mounting */ module.exports.FollowApp = FollowApp; - -// diff --git a/app/scripts/components/code-playground.js b/app/scripts/components/code-playground.js index 0e69d57..b617c8c 100644 --- a/app/scripts/components/code-playground.js +++ b/app/scripts/components/code-playground.js @@ -1,5 +1,6 @@ var ace = require('brace'); var React = require('react'); +var FCActions = require('../actions/fc-actions'); require('brace/theme/monokai'); //require('brace/theme/github'); //require('brace/theme/tomorrow'); @@ -49,26 +50,28 @@ var CodePlayground = React.createClass({ name : 'brace-editor', mode : 'javascript', theme : 'monokai', - height : '500px', - width : '500px', + height : '90%', + width : '620px', fontSize : 12, value: '', showGutter : true, onChange : null, - onLoad : null + onLoad : null, + wrapBehaviour: false }; }, setInitialState: function(){ return { queue: [''] } }, - onChange: function(evt) { - this.props.updateEditor(evt); + captureCodingEvent: function(evt) { + FCActions.captureCodingEvent(evt); }, componentWillReceiveProps: function(nextProps) { this.editor = ace.edit(nextProps.name); this.editor.getSession().setMode('ace/mode/'+nextProps.mode); this.editor.setTheme('ace/theme/'+nextProps.theme); this.editor.setFontSize(nextProps.fontSize); + this.editor.setWrapBehavioursEnabled(nextProps.wrapBehaviour); this.editor.setValue(nextProps.value); this.editor.renderer.setShowGutter(nextProps.showGutter); if (nextProps.onLoad) { @@ -81,7 +84,7 @@ var CodePlayground = React.createClass({ width: this.props.width, height: this.props.height }; - return (
); + return (
); }, componentDidMount: function() { @@ -90,7 +93,7 @@ var CodePlayground = React.createClass({ this.editor.getSession().setMode('ace/mode/'+this.props.mode); this.editor.setTheme('ace/theme/'+this.props.theme); this.editor.setFontSize(this.props.fontSize); - this.editor.on('change', this.onChange); + this.editor.on('change', this.captureCodingEvent); this.editor.setValue(this.props.value); this.editor.renderer.setShowGutter(this.props.showGutter); diff --git a/app/scripts/components/end-recording.js b/app/scripts/components/end-recording.js deleted file mode 100644 index d16cd01..0000000 --- a/app/scripts/components/end-recording.js +++ /dev/null @@ -1,21 +0,0 @@ -var React = require('react'); -var FCActions = require('../actions/fc-actions'); - -var EndRecording = React.createClass({ - propTypes: { - onClick: React.PropTypes.func - }, - - handleClick: function(){ - FCActions.stopRecording(); - //FCActions.createVideoFormat(); - }, - - render: function(){ - return ( - - ) - } -}); - -module.exports = EndRecording; diff --git a/app/scripts/components/player/player.js b/app/scripts/components/player/player.js new file mode 100644 index 0000000..f6eef5c --- /dev/null +++ b/app/scripts/components/player/player.js @@ -0,0 +1,314 @@ +/** @jsx React.DOM */ +var React = require('react'); + +//********** +//** Player Component is semi-tangible component handler of synchronization between audio and video playback +//********** + +// Stores +var PlayerStore = require('../../stores/player'); +var AudioStore = require('../../stores/audio'); +var VideoStore = require('../../stores/video'); +var FCActions = require('../../actions/fc-actions'); + +// Components +var _ = require('lodash'); + +// Store's getter methods +function _getPlayer(){ + return { player: PlayerStore.getPlayer() }; +} +function _getAudio(){ + return { audio: AudioStore.getAudio() }; +} +function _getVideo(){ + return { video: VideoStore.getVideo() }; +} + +// Local cached variables for comparison +var audioIsPlaying = false; +var videoIsPlaying = false; + +var Player = React.createClass({ + getInitialState: function(){ + //return {player: {}, audio: {}}; + return { + player: _getPlayer(), + audio: _getAudio(), + video: _getVideo() + } + }, + + /** + * [Lifecycle method] invoked once immediately before the initial rendering occurs + * Set up listeners for specific store state changes + */ + componentWillMount: function() { + PlayerStore.addChangeListener(this._playerChange); + AudioStore.addChangeListener(this._audioChange); + VideoStore.addChangeListener(this._videoChange); + }, + + /** + * [Lifecycle method] invoked once immediately after the initial rendering occurs + * Set up listeners for DOM events + */ + componentDidMount: function(){ + var audioTag = this.refs.audioPlayer.getDOMNode(); + audioTag.addEventListener("timeupdate", this._changePlayTime, false); + }, + + /** + * [Lifecycle method] invoked once immediately before the initial rendering occurs + * NOTE: Where the action happens on pause/play state change + * Return value determines whether component should render + */ + shouldComponentUpdate: function(){ + + // Only update in these cases + // 1. Video recording and Audio recording are ready and a play event has been fired (compare old isPlaying to new isPlaying coming through pipeline) + // 2. A change in audioIsPlaying has occurred + // 3. A change in videoIsPlaying has occurred + + // Case 1.) + if (this.state.video.isReady && this.state.audio.isReady && + audioIsPlaying === false && this.state.audio.isPlaying){ + audioIsPlaying = true; + this._createAndStartPlayer(); + return true; + } + + //TODO: @elGrecode combine check for audio and video. Two is (maybe) negligible and redundant + // Case 2a.) Audio Pause - old state(isPlaying) / new state(isNotPlaying) + //if (audioIsPlaying === true && this.state.audio.isPlaying === false){ + // console.log('pausing audio - SWITCHING STATE'); + // audioTag.pause(); + // audioIsPlaying = false; + // return true; + //// Case 2b.) Audio Play - oldState(isNotPlaying) / newState(isNotPlaying) + //} else if (audioIsPlaying === false && this.state.audio.isPlaying === true){ + // audioTag.play(); + // audioIsPlaying = true; + // return true; + //} + + // Case 3a.) Video Pause - old state(isPlaying) / new state(isNotPlaying) + if (videoIsPlaying === true && this.state.video.isPlaying === false){ + console.log('DEBUG**: Going to loop over these interval Ids', this.state.video.playbackIntervalIds); + this._clearPlaybackIntervals(this.state.video.playbackIntervalIds); + this._capturePausedVideoPlaybackTime(Date.now() - this.state.video.playbackStartTime); + this._capturePausedVideoState(this.props.editor.getValue()); + this._pauseVideo(); + videoIsPlaying = false; + return true; + // Case 3b.) Video Play - oldState(isNotPlaying) / newState(isNotPlaying) + } else if (videoIsPlaying === false && this.state.video.isPlaying === true){ + this._startPlayingVideo(); + this._startPlayingAudio(); + videoIsPlaying = true; + return true; + } + + return false; + }, + + /** + * [Lifecycle method] invoked once immediately after re-rendering occurs + * Starts audio and video playback + */ + //componentDidUpdate: function(){ + // + //}, + + //*** Private methods ***// + /** + * Sets local player state + * @private + */ + _playerChange: function() { + this.setState(_getPlayer()); + }, + + /** + * Sets local audio state + * @private + */ + _audioChange: function(){ + this.setState( _getAudio()); + }, + + /** + * Sets local video state + * @private + */ + _videoChange: function(){ + this.setState( _getVideo()); + }, + + /** + * Changes the time state of audio playback + * @param evt - playback audio event on time update + * @private + */ + _changePlayTime: function(evt){ + //console.log('audio player component', evt.target); + //console.log('audio total duration', evt.target.duration); + }, + + /** + * Creates and starts the screencast A/V player + * @private + */ + // todo: Rename to just create, refactor to remove initial play. Play should only come from state change + _createAndStartPlayer: function(){ + // Initialize + this.props.editor.setValue(""); + var audioTag = this.refs.audioPlayer.getDOMNode(); + }, + + /** + * Callback updating the time of the audio + * TODO: maybe send this up to the audio object store + * @private + */ + _timeUpdate: function( evt ){ + this.setState({playbackTime: evt.target.currentTime}) + }, + + _pauseVideo: function(){ + var audioElem = this.refs.audioPlayer.getDOMNode(); + audioElem.pause(); + }, + + /** + * Calculates the replaying of our videoEvent log + * @private + */ + _startPlayingVideo: function(){ + this._playVideoFromTime(this.state.video.pausedVideoStateText, this.state.video.pausedVideoTime, + this.state.video.playableVideo, this.props.editor); + }, + + /** + * Plays a video from a given point in time + * @param startingText + * @param startingTime + * @param videoEvents + * @param editor + * @private + */ + _playVideoFromTime: function( startingText, startingTime, videoEvents, editor){ + var that = this; + var TICKINCREMENT = 1000; + var editorDocument = editor.getSession().getDocument(); + var tick = this.state.video.currentTick; + var firstTick = tick; + var tickPartial = startingTime % 1000; + + editor.setValue(startingText); + editor.blur(); + window.editor = editor; + var startTime = Date.now(); + var playbackIntervalId = setInterval(function(){ // Every tick interval, set up queue events to fire + FCActions.setCurrentTick(tick); + + console.log('tick: ' + tick); + var eventsForTickArr = videoEvents[tick]; + if (eventsForTickArr){ // If we have events for this tick + var mskeys = Object.keys(eventsForTickArr); + + mskeys.forEach(function(eventTime, index){ + if (firstTick === tick){ // special first tick case + if (eventTime > tickPartial) { + console.log('FIRST TICK!'); + setTimeout(function () { + if (that.state.video.isPlaying) { + editorDocument.applyDeltas([eventsForTickArr[eventTime]]); + } else { + console.log('DEBUG no longer playing so not lingering with', eventsForTickArr[eventTime]) + } + }, eventTime); + } + } else { + setTimeout(function(){ + if (that.state.video.isPlaying){ + editorDocument.applyDeltas([ eventsForTickArr[eventTime] ]); + } else { + console.log('DEBUG no longer playing so not lingering with', eventsForTickArr[eventTime]) + } + }, eventTime); + } + + }); + } + + tick++; + }, TICKINCREMENT); + // Don't play forever + var playbackEndId = setTimeout(function(){ + FCActions.pauseVideo(); // clear playbackInterval after finished + }, this.state.video.lastEventTime + 1000); + + // Register the playbackStartTime and playbackIntervalId globally + // TODO: rename to reflect the fact that we really own need the in between tick time not the entire time. + FCActions.registerPlaybackStartTime(startTime); + FCActions.registerPlaybackIntervalIds([playbackIntervalId, playbackEndId]); + }, + + _startPlayingAudio: function(){ + // Audio playing time is contingent on video which requires more accuracy + var audioElem = this.refs.audioPlayer.getDOMNode(); + this._playAudioFromTime(audioElem, this.state.video.pausedVideoTime, this.state.video.currentTick); + }, + + _playAudioFromTime: function(audioElem, startTime, currentTick){ + var currentTime = (currentTick) + (startTime % 1000 / 1000); + setTimeout(function(){ + audioElem.play(); + audioElem.currentTime = currentTime; + }, 1000); // Need a second to account for setInterval of videoPlayer + }, + + /** + * Clears a given event loop interval + * @param intervalId + * @private + */ + _clearPlaybackIntervals: function( intervalIds ){ + _.each(intervalIds, function(id){ + clearInterval(id); + }); + }, + + /** + * Captures the current text of the paused video + * @param videoText + * @private + */ + _capturePausedVideoState: function( videoText ){ + FCActions.capturePausedVideoState(videoText); + }, + + /** + * Captures the current time of the paused video + * @param currentVideoTime + * @private + */ + _capturePausedVideoPlaybackTime: function( currentVideoTime ){ + FCActions.capturePausedVideoTime(currentVideoTime); + }, + + render: function(){ + // todo: make sure that we are getting access to the audio file + //console.log('rerendering -> here is the audiFile->', this.props.audioFile); + return ( +
+ +
+ ) + } +}); + +module.exports = Player; diff --git a/app/scripts/components/recording_actions/pause-recording-btn.js b/app/scripts/components/recording_actions/pause-recording-btn.js new file mode 100644 index 0000000..a0e6085 --- /dev/null +++ b/app/scripts/components/recording_actions/pause-recording-btn.js @@ -0,0 +1,30 @@ +/** @jsx React.DOM */ +var React = require('react'); +var FCActions = require('../../actions/fc-actions'); + +var PauseRecordingBtn = React.createClass({ + propTypes: { + audioIsPlaying: React.PropTypes.bool + }, + + /** + * Handles click and broadcasts pauses audio and video + */ + handleClick: function(){ + FCActions.pauseAudio(); + FCActions.pauseVideo(); + }, + + render: function(){ + // If we audio playing - display the pause button + var display = this.props.audioIsPlaying ? {display: 'inline-block'} : {display: 'none'}; + + return ( + + + + ) + } +}); + +module.exports = PauseRecordingBtn; diff --git a/app/scripts/components/recording_actions/recording-actions.js b/app/scripts/components/recording_actions/recording-actions.js new file mode 100644 index 0000000..25f1c0e --- /dev/null +++ b/app/scripts/components/recording_actions/recording-actions.js @@ -0,0 +1,74 @@ +/** @jsx React.DOM */ +var React = require('react'); + +// Stores +var RecorderStore = require('../../stores/recorder'); +var AudioStore = require('../../stores/audio'); +var VideoStore = require('../../stores/video'); + +// Components +var RecordingState = require('./recording-state.js'); +var ReplayRecordingBtn = require('./replay-recording-btn.js'); +var StopRecordingBtn = require('./stop-recording-btn'); +var StartRecordingBtn = require('./start-recording-btn'); +var PauseRecordingBtn = require('./pause-recording-btn'); +var Visualizer = require('./visualizer'); + +// Access state methods +function _getRecorder(){ + return { recorder: RecorderStore.getRecorder() }; +} +function _getAudio(){ + return { audio: AudioStore.getAudio() }; +} +function _getVideo(){ + return { video: VideoStore.getVideo() }; +} + +var RecordingActions = React.createClass({ + getInitialState: function(){ + return { + recorder: _getRecorder().recorder, + video: _getVideo().video, + audio: _getAudio().audio + } + }, + + + /** + * Register specific changes in state + */ + componentWillMount: function() { + RecorderStore.addChangeListener(this._recorderChange); + AudioStore.addChangeListener(this._audioChange); + VideoStore.addChangeListener(this._videoChange); + }, + + /*** State setters ***/ + _recorderChange: function() { + this.setState(_getRecorder()); + }, + _audioChange: function(){ + this.setState(_getAudio()); + }, + _videoChange: function(){ + this.setState(_getVideo()) + }, + + render: function(){ + return ( +
+
+ + + + + + +
+
+ ) + } +}); + +module.exports = RecordingActions; diff --git a/app/scripts/components/recording_actions/recording-state.js b/app/scripts/components/recording_actions/recording-state.js new file mode 100644 index 0000000..09880b7 --- /dev/null +++ b/app/scripts/components/recording_actions/recording-state.js @@ -0,0 +1,19 @@ +/** @jsx React.DOM */ +var React = require('react'); + +var RecordingState = React.createClass({ + propTypes: { + isRecording: React.PropTypes.bool + }, + + render: function(){ + var recording = this.props.isRecording ? 'recording' : ''; + return ( +
+ +
+ ) + } +}); + +module.exports = RecordingState; diff --git a/app/scripts/components/recording_actions/replay-recording-btn.js b/app/scripts/components/recording_actions/replay-recording-btn.js new file mode 100644 index 0000000..0b670a5 --- /dev/null +++ b/app/scripts/components/recording_actions/replay-recording-btn.js @@ -0,0 +1,31 @@ +/** @jsx React.DOM */ +var React = require('react'); +var FCActions = require('../../actions/fc-actions'); + +var ReplayBtn = React.createClass({ + propTypes: { + videoIsReady: React.PropTypes.bool, + audioIsReady: React.PropTypes.bool, + audioIsPlaying: React.PropTypes.bool + }, + + /** + * Handles click on Replay Recording Btn and starts screencast + */ + handleClick: function(){ + FCActions.playbackAudio(); + FCActions.playbackVideo(); + }, + + render: function(){ + // If we have a video ready, audio ready, and are not currently playing - display the playback button + var display = this.props.videoIsReady && this.props.audioIsReady && !this.props.audioIsPlaying ? {display: 'inline-block'} : {display: 'none'}; + return ( + + + + ) + } +}); + +module.exports = ReplayBtn; diff --git a/app/scripts/components/recording_actions/start-recording-btn.js b/app/scripts/components/recording_actions/start-recording-btn.js new file mode 100644 index 0000000..ecbbb50 --- /dev/null +++ b/app/scripts/components/recording_actions/start-recording-btn.js @@ -0,0 +1,33 @@ +/** @jsx React.DOM */ +var React = require('react'); +var FCActions = require('../../actions/fc-actions'); + +var StartRecordingBtn = React.createClass({ + propTypes: { + recordingIsAllowable: React.PropTypes.bool, + isRecording: React.PropTypes.bool, + audioIsPlaying: React.PropTypes.bool + }, + + /** + * Handles click on StartRecordingBtn + */ + startRecording: function(){ + FCActions.startRecordingVideo(); + FCActions.startRecordingAudio(); + }, + + render: function(){ + // If recording is allowable for this browser, and we are not currently recording or playing + var display = this.props.recordingIsAllowable && !this.props.isRecording && !this.props.audioIsPlaying ? + {display: 'inline-block'} : {display: 'none'}; + + return ( + + + + ) + } +}); + +module.exports = StartRecordingBtn; diff --git a/app/scripts/components/recording_actions/stop-recording-btn.js b/app/scripts/components/recording_actions/stop-recording-btn.js new file mode 100644 index 0000000..bd6f701 --- /dev/null +++ b/app/scripts/components/recording_actions/stop-recording-btn.js @@ -0,0 +1,30 @@ +/** @jsx React.DOM */ +var React = require('react'); +var FCActions = require('../../actions/fc-actions'); + +var StopRecordingBtn = React.createClass({ + propTypes: { + isRecording: React.PropTypes.bool + }, + + /** + * Handles click on StopRecordingBtn + */ + handleClick: function(){ + FCActions.stopRecordingVideo(); + FCActions.stopRecordingAudio(); + //FCActions.createAudioPlayback(); + //FCActions.createVideoFormat(); + }, + + render: function(){ + var display = this.props.isRecording ? {display: 'inline-block'} : {display: 'none'}; + return ( + + + + ) + } +}); + +module.exports = StopRecordingBtn; diff --git a/app/scripts/components/recording_actions/visualizer.js b/app/scripts/components/recording_actions/visualizer.js new file mode 100644 index 0000000..8958fd0 --- /dev/null +++ b/app/scripts/components/recording_actions/visualizer.js @@ -0,0 +1,112 @@ +/** @jsx React.DOM */ +var React = require('react'); +var FCActions = require('../../actions/fc-actions'); + +// Stores +var RecorderStore = require('../../stores/recorder'); +var AudioStore = require('../../stores/audio'); + +// Local cache for local calculations +var previousPhase = undefined; +var currentPhase = undefined; +var firstTime = undefined; +var analyserNode = null; +var zeroGain = null; +var analyserContext = null; + +function _isBeginningPhase( currentPhase ){ + if (previousPhase === 'unstarted' && currentPhase === 'started') { return true; } + else { return false; } +} + +function _isMainPhase( currentPhase ){ + if (previousPhase === 'started' && currentPhase === 'main') { return true; } + else { return false; } +} + +var Visualizer = React.createClass({ + + _handlePhase: function(){ + currentPhase = this.props.phase; + + // Handle each phase + if ( _isBeginningPhase( currentPhase ) ){ + console.log('we are in the beginning phase'); + this._createAudioVisualizer(); + + setTimeout(function(){ + FCActions.startMainPhaseRecording(); + }, 3000) + } else if ( _isMainPhase( currentPhase ) ){ + console.log('main phase'); + // deregister event listener + } + previousPhase = currentPhase + }, + + _updateAnalysers: function(time) { + // If we don't have a firstTime and time is a number + if (!firstTime && !isNaN(parseFloat(time)) && isFinite(time)) { firstTime = time; } + // Normalized time from first start of updating analysers + if (time - firstTime > 5000) { return; } + if (!analyserContext) { + + var canvas = this.refs.canvas.getDOMNode(); + canvasWidth = canvas.width; + canvasHeight = canvas.height; + analyserContext = canvas.getContext('2d'); + } + + var SPACING = 12; + var BAR_WIDTH = 12; + var numBars = Math.round(canvasWidth / SPACING); + var freqByteData = new Uint8Array(analyserNode.frequencyBinCount); + + analyserNode.getByteFrequencyData(freqByteData); + + analyserContext.clearRect(0, 0, canvasWidth, canvasHeight); + analyserContext.fillStyle = '#912590'; + analyserContext.lineCap = 'round'; + var multiplier = analyserNode.frequencyBinCount / numBars; + var step = Math.round(freqByteData.length / numBars); + // Draw rectangle for each frequency bin. + for (var i = 0; i < numBars; ++i) { + + var value = freqByteData[i * step]; + var magnitude = 0; + var offset = Math.floor( i * multiplier ); + for (var j = 0; j< multiplier; j++) + magnitude += freqByteData[offset + j]; + magnitude = magnitude / multiplier; + analyserContext.fillRect(i * (SPACING + 4), canvasHeight - (value / 10), BAR_WIDTH, 40); + } + + + window.requestAnimationFrame( this._updateAnalysers ); + }, + + /** + * Create a brief visualization of our in progress audio recording + */ + _createAudioVisualizer: function(){ + var audio = this.props.audio; + analyserNode = audio.audioContext.createAnalyser(); + analyserNode.fftSize = 2048; + audio.sound.connect( analyserNode ); + + zeroGain = audio.audioContext.createGain(); + zeroGain.gain.value = 0.0; + audio.sound.connect( zeroGain ); + zeroGain.connect( audio.audioContext.destination ); + this._updateAnalysers(); + }, + + render: function(){ + this._handlePhase(); + return ( + + ) + } +}); + +module.exports = Visualizer; diff --git a/app/scripts/constants/fc-constants.js b/app/scripts/constants/fc-constants.js index 32edbd9..7a127da 100644 --- a/app/scripts/constants/fc-constants.js +++ b/app/scripts/constants/fc-constants.js @@ -1,7 +1,39 @@ module.exports = { // Map a list of actions - STOP_RECORDING : 'STOP_RECORDING', - CREATE_VIDEO_FORMAT : 'CREATE_VIDEO_FORMAT' + //todo: possibly register these actions not as view actions (look into) + REGISTER_VAI : 'REGISTER_VAI', + REGISTER_AUDIO : 'REGISTER_AUDIO', + REGISTER_RECORDER : 'REGISTER_RECORDER', + REGISTER_PLAYBACK_INTERVAL_IDS: 'REGISTER_PLAYBACK_INTERVAL_IDS', + REGISTER_PLAYBACK_START_TIME : 'REGISTER_PLAYBACK_START_TIME', + RECORDER_NOT_ALLOWABLE : 'RECORDER_NOT_ALLOWABLE', + + START_RECORDING_VIDEO : 'START_RECORDING_VIDEO', + START_RECORDING_AUDIO : 'START_RECORDING_AUDIO', + MAIN_PHASE_RECORDING : 'MAIN_PHASE_RECORDING', + + CODING_EVENT : 'CODING_EVENT', + + STOP_RECORDING_VIDEO : 'STOP_RECORDING_VIDEO', + STOP_RECORDING_AUDIO : 'STOP_RECORDING_AUDIO', + REGISTER_AUDIO_FILE : 'REGISTER_AUDIO_FILE', + CREATE_VIDEO_FORMAT : 'CREATE_VIDEO_FORMAT', + PLAYBACK_VIDEO : 'PLAYBACK_VIDEO', + CURRENT_TICK : 'CURRENT_TICK', + PLAYBACK_AUDIO : 'PLAYBACK_AUDIO', + + // Playback actions + //PLAY_AUDIO : 'PAUSE_AUDIO' + PAUSE_AUDIO : 'PAUSE_AUDIO', + PAUSE_VIDEO : 'PAUSE_VIDEO', + PAUSED_VIDEO_STATE : 'PAUSED_VIDEO_STATE', + PAUSED_VIDEO_TIME : 'PAUSED_VIDEO_TIME' + // PLAY_SCREENCAST + // PAUSE_SCREENCAST + // SAVE_SCREENCAST? + + + //ADD_ITEM: 'ADD_ITEM', //REMOVE_ITEM: 'REMOVE_ITEM', //INCREASE_ITEM: 'INCREASE_ITEM', diff --git a/app/scripts/dispatcher/dispatcher.js b/app/scripts/dispatcher/dispatcher.js index b6e452a..371d27a 100644 --- a/app/scripts/dispatcher/dispatcher.js +++ b/app/scripts/dispatcher/dispatcher.js @@ -1,4 +1,4 @@ -var Promise = require('es6-promise').Promise; +var Promise = require('es6-promise-polyfill').Promise; var _ = require('lodash'); var _callbacks = []; diff --git a/app/scripts/stores/__tests__/audio-test.js b/app/scripts/stores/__tests__/audio-test.js new file mode 100644 index 0000000..eaa837d --- /dev/null +++ b/app/scripts/stores/__tests__/audio-test.js @@ -0,0 +1,91 @@ +jest.dontMock('../audio'); +jest.dontMock('../../dispatcher/fc-dispatcher'); + +var AudioStore = require('../audio'); +var FCConstants = require('../../constants/fc-constants'); +var FCDispatcher = require('../../dispatcher/fc-dispatcher'); +var AppDispatcher = require('../../dispatcher/dispatcher'); + +describe('AudioStoreEvents', function() { + var callback; + + + var registerAudio = { + actionType: FCConstants.REGISTER_AUDIO, + audio: require('../../async/audio') + }; + + var registerAudioFile = { + actionType: FCConstants.REGISTER_AUDIO_FILE, + audioFile: 'blobFile.wav' + }; + + var playbackAudio = { + actionType: FCConstants.PLAYBACK_AUDIO + }; + + var pauseAudio = { + actionType: FCConstants.PAUSE_AUDIO + }; + + beforeEach(function() { + dispatcher = new AppDispatcher(); + callback = jest.genMockFunction(); + }); + //it('expects audio to be registered', function(){ + // dispatcher.register(function(payload) { + // callback(payload); + // }); + // di + // + // expect(callback.mock.calls.length).toBe(1); + // + // callback(registerAudio); + // var audio = AudioStore.getAudio(); + // console.log(audio); + // expect(audio.isPlaying).toBe(false); + + //expect(callback.mock.calls.length).toBe(1); + //expect(callback.mock.calls[0][0]).toBe(payload); + + //}); + + it('expects an audio object to be registered', function() { + FCDispatcher.handleViewAction(registerAudio); + + var audio = AudioStore.getAudio(); + expect(audio).toBeDefined(); + expect(audio.isPlaying).toBe(false); + + }); + + it('expects an audio file object to be registered upon completion', function() { + FCDispatcher.handleViewAction(registerAudioFile); + + var audio = AudioStore.getAudio(); + expect(audio.audioFile).toEqual('blobFile.wav'); + expect(audio.isReady).toBe(true); + + }); + + it('should start playing audio when played', function() { + FCDispatcher.handleViewAction(playbackAudio); + + var audio = AudioStore.getAudio(); + expect(audio.isPlaying).toBe(true); + + }); + + it('should pause audio when paused', function() { + FCDispatcher.handleViewAction(pauseAudio); + + var audio = AudioStore.getAudio(); + expect(audio.isPlaying).toBe(false); + + }); + + + + + +}); diff --git a/app/scripts/stores/audio.js b/app/scripts/stores/audio.js new file mode 100644 index 0000000..691b2da --- /dev/null +++ b/app/scripts/stores/audio.js @@ -0,0 +1,118 @@ +/** + * Audio Store manages our audio data + */ + +// --- Dependencies --- // +var _ = require('lodash'); +var EventEmitter = require('events').EventEmitter; +var FCConstants = require('../constants/fc-constants'); +var FCDispatcher = require('../dispatcher/fc-dispatcher'); + +// Managing a private audio data +var _audio = {}; + + +// --- Private Store Methods --- // +/** + * Mutable function registers a reference to a previously created audio object + * @param {object} audio + */ +function _registerAudio( audio ){ + _audio = audio; + _audio.isPlaying = false; + console.log('the current audio->', _audio); +} + +/** + * Mutable function registers the url path of the audio blob in local storage + * @param audioFile + */ +function _registerAudioFile( audioFile ){ + _audio.audioFile = audioFile; + _audio.isReady = true; +} + +/** + * Mutable functions changing audio's isPlaying state + */ +function _playbackAudio(){ + _audio.isPlaying = true; +} +function _pauseAudio(){ + _audio.isPlaying = false; +} + +// --- Public Store Methods --- // +var CHANGE_EVENT = 'change'; + +var AudioStore = _.extend(EventEmitter.prototype, { + // Really the only thing we want to do is notify view of audio creation + // Alternatively, if we were to ever have an audio visualizer, this might be something that we notify view + emitChange: function() { + this.emit(CHANGE_EVENT); + }, + + /** + * @param {function} callback + */ + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + + /** + * @param {function} callback + */ + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + + /** + * Returns the current audio state + * @returns {object} _audio + */ + getAudio: function(){ + return _audio; + }, + + dispatcherIndex: FCDispatcher.register(function(payload) { + var action = payload.action; + var audio = action.audio || ''; + + switch(action.actionType) { + case FCConstants.REGISTER_AUDIO: + _registerAudio(audio); + AudioStore.emitChange(); + break; + + case FCConstants.REGISTER_AUDIO_FILE: + _registerAudioFile(action.audioFile); + AudioStore.emitChange(); + break; + + case FCConstants.PLAYBACK_AUDIO: + _playbackAudio(); + AudioStore.emitChange(); + break; + + case FCConstants.PAUSE_AUDIO: + _pauseAudio(); + AudioStore.emitChange(); + break; + + //case FCConstants.START_RECORDING_AUDIO: + // createAudioVisualizer(); + // AudioStore.emitChange(); + // break; + // add more cases for other actionTypes + } + + return true; // No errors. Needed by promise in Dispatcher. + }) +}); + + +module.exports = AudioStore; + + + + diff --git a/app/scripts/stores/player.js b/app/scripts/stores/player.js new file mode 100644 index 0000000..0e114e2 --- /dev/null +++ b/app/scripts/stores/player.js @@ -0,0 +1,78 @@ +/** + * Player Store manages our playback synchronicity between audio and video + */ + +// --- Dependencies --- // +var _ = require('lodash'); +var EventEmitter = require('events').EventEmitter; +var FCConstants = require('../constants/fc-constants'); +var FCDispatcher = require('../dispatcher/fc-dispatcher'); + +// Managing a private player state +var _player = { + currentTime: 0 +}; + + +// --- Private Store Methods --- // +// Private store methods go here +function _changePlayTime( currentTime ){ + _player.currentTime = currentTime; +} + +// --- Public Store Methods --- // +var CHANGE_EVENT = 'change'; +var PlayerStore = _.extend(EventEmitter.prototype, { + /** + * Emitting change events + */ + emitChange: function() { + this.emit(CHANGE_EVENT); + }, + + /** + * @param {function} callback + */ + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + + /** + * @param {function} callback + */ + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + + /** + * Returns the current player state + * @returns {object} _player + */ + getPlayer: function(){ + return _player; + }, + + dispatcherIndex: FCDispatcher.register(function(payload) { + var action = payload.action; + + switch(action.actionType) { + case FCConstants.CHANGE_PLAY_TIME: + console.log('payload in player change', payload); + _changePlayTime(payload.currentTime); + PlayerStore.emitChange(); + console.log('emitting from playerStore...') + break; + + // add more cases for other actionTypes + } + + return true; // No errors. Needed by promise in Dispatcher. + }) +}); + + +module.exports = PlayerStore; + + + + diff --git a/app/scripts/stores/recorder.js b/app/scripts/stores/recorder.js new file mode 100644 index 0000000..b671ee7 --- /dev/null +++ b/app/scripts/stores/recorder.js @@ -0,0 +1,194 @@ +/** + * Recorder Store manages our recorder object and data and through API, our audio + */ + +// --- Dependencies --- // +var _ = require('lodash'); +var EventEmitter = require('events').EventEmitter; +var FCActions = require('../actions/fc-actions'); +var FCConstants = require('../constants/fc-constants'); +var FCDispatcher = require('../dispatcher/fc-dispatcher'); + +// A recorder data object that manages the API for a recording +var _recorder = {}; + +//todo: move this visualization into the component directory +/** + * Draw the completed audio into a wave graph + * @param width + * @param height + * @param context + * @param data + */ +function drawBuffer( width, height, context, data ) { + console.log('drawing buffer'); + var step = Math.ceil( data.length / width ); + var amp = height / 2; + context.fillStyle = "silver"; + context.clearRect(0,0,width,height); + for(var i=0; i < width; i++){ + var min = 1.0; + var max = -1.0; + for (j=0; j max) + max = datum; + } + context.fillRect(i,(1+min)*amp,1,Math.max(1,(max-min)*amp)); + } +} + + +// --- Private Store Methods --- // +/** + * Callback function that allows manipulation of buffers + * @param {object} blob (raw audio object) + */ +function exportBuffers( buffers ) { + // todo: draw this audio recording on the screen + // do I want to draw this for visual display? + var canvas = document.getElementById( "wavedisplay" ); + drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] ); + + // the ONLY time exportBuffers is called is right after a new recording is completed - + // so here's where we currently should set up the download. + _recorder.exportWAV( doneEncoding ); +} + +/** + * Callback function that prepares and downloads raw audio file + * @param {object} blob (raw audio object) + */ +function doneEncoding( blob ) { + console.log(_recorder); + console.log(blob); + var audioPlaybackSrc = _recorder.setupDownload( blob, "Audio" + ((_recorder.recorderIndex<10)?"0":"") + _recorder.recorderIndex + ".wav" ); + FCActions.registerAudioFile(audioPlaybackSrc); + _recorder.recorderIndex++; +} + +/** + * Mutable function registers a reference to a previously created recorder object and attach local state + * @param {object} recorder + */ +function registerRecorder( recorder ){ + _recorder = recorder; + _recorder.isAllowable = true; + _recorder.isRecording = false; + _recorder.phase = "unstarted"; + _recorder.recorderIndex = 0; +} + +/** + * Mutable function sets isAllowable state on recorder object + */ +function recorderNotAllowable(){ + _recorder.isAllowable = false; +} + +/** + * Starts the recording of the recorder object + */ +function startRecording(){ + if (!_recorder.isRecording){ + _recorder.clear(); + _recorder.record(); + _recorder.isRecording = true; + _recorder.phase = "started"; + } +} + +/** + * A hook for starting/signalling the main phase of recording + */ +function startMainPhase(){ + _recorder.phase = "main"; +} + +/** + * Stops the recording of the recorder object + */ +function stopRecording(){ + if (_recorder.isRecording){ + console.log('stopping recorder object'); + _recorder.stop(); + _recorder.getBuffers(exportBuffers); + _recorder.isRecording = false; + _recorder.phase = "stopped"; + } +} + +// --- Public Store Methods --- // +var CHANGE_EVENT = 'change'; +var RecorderStore = _.extend(EventEmitter.prototype, { + emitChange: function(){ + this.emit(CHANGE_EVENT); + }, + + /** + * @param {function} callback + */ + addChangeListener: function( callback ){ + this.on(CHANGE_EVENT, callback); + }, + + /** + * @param {function} callback + */ + removeChangeListener: function( callback ){ + this.removeListener(CHANGE_EVENT, callback); + }, + + getRecorder: function(){ + return _recorder; + }, + + dispatcherIndex: FCDispatcher.register(function(payload) { + var action = payload.action; + var recorder = action.recorder || ''; + + switch(action.actionType) { + case FCConstants.REGISTER_VAI: + registerVAI(payload.rafId); + RecorderStore.emitChange(); + break; + + case FCConstants.REGISTER_RECORDER: + // todo: check to make sure we have a valid recording + registerRecorder(recorder); + RecorderStore.emitChange(); + break; + + case FCConstants.RECORDER_NOT_ALLOWABLE: + recorderNotAllowable(); + break; + + case FCConstants.START_RECORDING_AUDIO: + startRecording(); + RecorderStore.emitChange(); + break; + + case FCConstants.MAIN_PHASE_RECORDING: + startMainPhase(); + RecorderStore.emitChange(); + break; + + case FCConstants.STOP_RECORDING_AUDIO: + stopRecording(); + RecorderStore.emitChange(); + break; + + // add more cases for other actionTypes + } + return true; // No errors. Needed by promise in Dispatcher. + }) +}); + + +module.exports = RecorderStore; + + + + diff --git a/app/scripts/stores/video.js b/app/scripts/stores/video.js new file mode 100644 index 0000000..a45a141 --- /dev/null +++ b/app/scripts/stores/video.js @@ -0,0 +1,270 @@ +/** + * Video Store manages our video data + */ + +// --- Dependencies --- // +var _ = require('lodash'); +var EventEmitter = require('events').EventEmitter; +var FCConstants = require('../constants/fc-constants'); +var FCDispatcher = require('../dispatcher/fc-dispatcher'); + + +function Video( startTime ){ + //this.startState = startState; + this.startTime = startTime; + this.isRecording = false; + this.isPlaying = false; + this.recordedEvents = {}; + this.playbackIntervalIds = undefined; + this.playbackStartTime = undefined; + this.pausedVideoStateText = ''; // current text of paused video + this.pausedVideoTime = 0; // current time of paused video + this.currentTick = 0; + return this; +} + +_video = {}; + +// --- Private Store Methods --- // + +/** + * Pass in a video object with raw events and convert to a playable video with ticks + * @args {object} video (Video object) + * @return {object} A converted video object + */ +function _processEvents( video ){ + console.log('processing video events'); + var videoEvents = {}; + + // *Takes events in eventQueue and creates a playable video + var TICKINCREMENT = 1000; + var totalTicks = Math.ceil(video.lastEventTime / TICKINCREMENT); + var eventQueue = video.recordedEvents; + + // for each tick, determine how many events occurred + for (var tick = 0; tick < totalTicks; tick++){ + var startOfTick = tick * TICKINCREMENT; + var endOfTick = startOfTick + TICKINCREMENT; + + // Iterate over the key events + var mskeys = Object.keys(eventQueue); + mskeys.forEach(function(eventTime){ + // loop over ms eventTimes checking for matches within tick range + if (eventTime >= startOfTick && eventTime < endOfTick){ + // {0: [{relativeTime: evt}, {relativeTime: evt}] } + // {1: [{relativeTime: evt}] } + // evenTime should be in ms relativeTime to tick + + var relativeTime = eventTime - startOfTick; + + if (tick in videoEvents){ // push event if tick is already present + // place event within tick with new relative time + videoEvents[tick][relativeTime] = eventQueue[eventTime]; + } else { // create tick with first event + // create object first + videoEvents[tick] = {}; + videoEvents[tick][relativeTime] = eventQueue[eventTime]; + } + } + }) + } + + console.log('videoEvents'); + console.log(videoEvents); + + console.log('eventQueue'); + console.log(eventQueue); + + return videoEvents; +} +/** + * Starts the video recording + * Mutable function that changes the state of _video + */ +function startRecording(){ + if (typeof _video.isRecording === 'undefined' || _video.isRecording === false){ + // If there is no video and we are not recording currently, we should create a video + _video = new Video(Date.now()); + _video.isRecording = true; + } +} + +/** + * registers the current tick of the video + * @param {Integer} tick + * @private + */ +function _registerCurrentTick( tick ){ + _video.currentTick = tick; +} + +/** + * Mutable function registers the interval id to be able to stop the playback event loop + * @param {Array} playbackIntervalIds + * @private + */ +function _registerIntervalIds( playbackIntervalIds ){ + _video.playbackIntervalIds = playbackIntervalIds; +} + +/** + * Mutable function registers the start time of when the video begins + * @param playbackStartTime + * @private + */ +function _registerPlaybackStartTime( playbackStartTime ){ + _video.playbackStartTime = playbackStartTime; +} + +/** + * Stops the video recording and stores a playable video + * Mutable function that changes the state of _video + */ +function stopRecording(){ + if (_video.isRecording === true){ + // If there is no video and we are not recording currently, we should create a video + _video.isRecording = false; + _video.playableVideo = _processEvents(_video); + _video.isReady = true; + } +} + +/** + * Records the event fired by editor when changed + * Mutable function that manipulates video state + * @args {object} evt - The event fired by the brace editor to be recorded + */ +function recordEvent( evt ){ + if (_video.isRecording){ + + // Calculate event time slot and store the event + var eventTime = Date.now(), + eventIndex = Math.floor((eventTime - _video.startTime)); + + _video.recordedEvents[eventIndex] = evt.data; + _video.lastEventTime = eventIndex; + } +} + +/** + * Mutable functions changing the playback state of the video to true + */ +function _playbackVideo(){ + if (!_video.isRecording && !_video.isPlaying){ + console.log('changing isPlaying property in _video'); + _video.isPlaying = true; + } +} +function _pauseVideo(){ + if (!_video.isRecording && _video.isPlaying){ + console.log('changing isPlaying property in _video to false'); + _video.isPlaying = false; + } +} + +/** + * Mutable function registering the video state after a pause + * @private + */ +function _capturePausedVideoState( videoText ){ + _video.pausedVideoStateText = videoText; +} + +/** + * Mutable function registering the video time after a pause + * @private + */ +function _capturePausedVideoTime( videoTime ){ + _video.pausedVideoTime = videoTime; +} + +// --- Public Store Methods --- // +var CHANGE_EVENT = 'change'; + +var VideoStore = _.extend(EventEmitter.prototype, { + // Change emitting events + emitChange: function() { + this.emit(CHANGE_EVENT); + }, + + /** + * @param {function} callback + */ + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + + /** + * @param {function} callback + */ + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + + /** + * Returns the current video state + * @returns {object} _video + */ + getVideo: function(){ + return _video; + }, + + dispatcherIndex: FCDispatcher.register(function(payload) { + var action = payload.action; + var video = action.video || ''; + + switch(action.actionType) { + case FCConstants.START_RECORDING_VIDEO: + startRecording(); + VideoStore.emitChange(); + break; + case FCConstants.CODING_EVENT: + recordEvent(payload.action.event); + VideoStore.emitChange(); + break; + case FCConstants.STOP_RECORDING_VIDEO: + stopRecording(); + VideoStore.emitChange(); + break; + case FCConstants.PLAYBACK_VIDEO: + _playbackVideo(); + VideoStore.emitChange(); + break; + case FCConstants.CURRENT_TICK: + _registerCurrentTick(payload.action.currentTick); + VideoStore.emitChange(); + break; + case FCConstants.REGISTER_PLAYBACK_INTERVAL_IDS: + _registerIntervalIds(payload.action.playbackIntervalIds); + VideoStore.emitChange(); + break; + case FCConstants.REGISTER_PLAYBACK_START_TIME: + _registerPlaybackStartTime(payload.action.playbackStartTime); + VideoStore.emitChange(); + break; + case FCConstants.PAUSE_VIDEO: + _pauseVideo(); + VideoStore.emitChange(); + break; + case FCConstants.PAUSED_VIDEO_STATE: + _capturePausedVideoState(payload.action.videoText); + VideoStore.emitChange(); + break; + case FCConstants.PAUSED_VIDEO_TIME: + _capturePausedVideoTime(payload.action.currentVideoTime); + VideoStore.emitChange(); + break; + + // add more cases for other actionTypes + } + + return true; // No errors. Needed by promise in Dispatcher. + }) +}); + + +module.exports = VideoStore; + + + + diff --git a/app/styles/ace.css b/app/styles/ace.css deleted file mode 100644 index 8d3ab58..0000000 --- a/app/styles/ace.css +++ /dev/null @@ -1,424 +0,0 @@ -.ace_editor { - position: relative; - overflow: hidden; - font: 12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - direction: ltr; -} - -.ace_scroller { - position: absolute; - overflow: hidden; - top: 0; - bottom: 0; - background-color: inherit; - -ms-user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - cursor: text; -} - -.ace_content { - position: absolute; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - min-width: 100%; -} - -.ace_dragging .ace_scroller:before{ - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - content: ''; - background: rgba(250, 250, 250, 0.01); - z-index: 1000; -} -.ace_dragging.ace_dark .ace_scroller:before{ - background: rgba(0, 0, 0, 0.01); -} - -.ace_selecting, .ace_selecting * { - cursor: text !important; -} - -.ace_gutter { - position: absolute; - overflow : hidden; - width: auto; - top: 0; - bottom: 0; - left: 0; - cursor: default; - z-index: 4; - -ms-user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; -} - -.ace_gutter-active-line { - position: absolute; - left: 0; - right: 0; -} - -.ace_scroller.ace_scroll-left { - box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset; -} - -.ace_gutter-cell { - padding-left: 19px; - padding-right: 6px; - background-repeat: no-repeat; -} - -.ace_gutter-cell.ace_error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg=="); - background-repeat: no-repeat; - background-position: 2px center; -} - -.ace_gutter-cell.ace_warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg=="); - background-position: 2px center; -} - -.ace_gutter-cell.ace_info { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII="); - background-position: 2px center; -} -.ace_dark .ace_gutter-cell.ace_info { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC"); -} - -.ace_scrollbar { - position: absolute; - right: 0; - bottom: 0; - z-index: 6; -} - -.ace_scrollbar-inner { - position: absolute; - cursor: text; - left: 0; - top: 0; -} - -.ace_scrollbar-v{ - overflow-x: hidden; - overflow-y: scroll; - top: 0; -} - -.ace_scrollbar-h { - overflow-x: scroll; - overflow-y: hidden; - left: 0; -} - -.ace_print-margin { - position: absolute; - height: 100%; -} - -.ace_text-input { - position: absolute; - z-index: 0; - width: 0.5em; - height: 1em; - opacity: 0; - background: transparent; - -moz-appearance: none; - appearance: none; - border: none; - resize: none; - outline: none; - overflow: hidden; - font: inherit; - padding: 0 1px; - margin: 0 -1px; - text-indent: -1em; - -ms-user-select: text; - -moz-user-select: text; - -webkit-user-select: text; - user-select: text; -} - -.ace_text-input.ace_composition { - background: inherit; - color: inherit; - z-index: 1000; - opacity: 1; - text-indent: 0; -} - -.ace_layer { - z-index: 1; - position: absolute; - overflow: hidden; - /* workaround for chrome bug https://github.com/ajaxorg/ace/issues/2312*/ - word-wrap: normal; - white-space: pre; - height: 100%; - width: 100%; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - /* setting pointer-events: auto; on node under the mouse, which changes - during scroll, will break mouse wheel scrolling in Safari */ - pointer-events: none; -} - -.ace_gutter-layer { - position: relative; - width: auto; - text-align: right; - pointer-events: auto; -} - -.ace_text-layer { - font: inherit !important; -} - -.ace_cjk { - display: inline-block; - text-align: center; -} - -.ace_cursor-layer { - z-index: 4; -} - -.ace_cursor { - z-index: 4; - position: absolute; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - border-left: 2px solid -} - -.ace_slim-cursors .ace_cursor { - border-left-width: 1px; -} - -.ace_overwrite-cursors .ace_cursor { - border-left-width: 0; - border-bottom: 1px solid; -} - -.ace_hidden-cursors .ace_cursor { - opacity: 0.2; -} - -.ace_smooth-blinking .ace_cursor { - -webkit-transition: opacity 0.18s; - transition: opacity 0.18s; -} - -.ace_editor.ace_multiselect .ace_cursor { - border-left-width: 1px; -} - -.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack { - position: absolute; - z-index: 3; -} - -.ace_marker-layer .ace_selection { - position: absolute; - z-index: 5; -} - -.ace_marker-layer .ace_bracket { - position: absolute; - z-index: 6; -} - -.ace_marker-layer .ace_active-line { - position: absolute; - z-index: 2; -} - -.ace_marker-layer .ace_selected-word { - position: absolute; - z-index: 4; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -.ace_line .ace_fold { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - display: inline-block; - height: 11px; - margin-top: -2px; - vertical-align: middle; - - background-image: - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="), - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII="); - background-repeat: no-repeat, repeat-x; - background-position: center center, top left; - color: transparent; - - border: 1px solid black; - border-radius: 2px; - - cursor: pointer; - pointer-events: auto; -} - -.ace_dark .ace_fold { -} - -.ace_fold:hover{ - background-image: - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="), - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC"); -} - -.ace_tooltip { - background-color: #FFF; - background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.1)); - background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1)); - border: 1px solid gray; - border-radius: 1px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); - color: black; - max-width: 100%; - padding: 3px 4px; - position: fixed; - z-index: 999999; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - cursor: default; - white-space: pre; - word-wrap: break-word; - line-height: normal; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - pointer-events: none; -} - -.ace_folding-enabled > .ace_gutter-cell { - padding-right: 13px; -} - -.ace_fold-widget { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - margin: 0 -12px 0 1px; - display: none; - width: 11px; - vertical-align: top; - - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg=="); - background-repeat: no-repeat; - background-position: center; - - border-radius: 3px; - - border: 1px solid transparent; - cursor: pointer; -} - -.ace_folding-enabled .ace_fold-widget { - display: inline-block; -} - -.ace_fold-widget.ace_end { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg=="); -} - -.ace_fold-widget.ace_closed { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA=="); -} - -.ace_fold-widget:hover { - border: 1px solid rgba(0, 0, 0, 0.3); - background-color: rgba(255, 255, 255, 0.2); - box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7); -} - -.ace_fold-widget:active { - border: 1px solid rgba(0, 0, 0, 0.4); - background-color: rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); -} -/** - * Dark version for fold widgets - */ -.ace_dark .ace_fold-widget { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC"); -} -.ace_dark .ace_fold-widget.ace_end { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg=="); -} -.ace_dark .ace_fold-widget.ace_closed { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg=="); -} -.ace_dark .ace_fold-widget:hover { - box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2); - background-color: rgba(255, 255, 255, 0.1); -} -.ace_dark .ace_fold-widget:active { - box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2); -} - -.ace_fold-widget.ace_invalid { - background-color: #FFB4B4; - border-color: #DE5555; -} - -.ace_fade-fold-widgets .ace_fold-widget { - -webkit-transition: opacity 0.4s ease 0.05s; - transition: opacity 0.4s ease 0.05s; - opacity: 0; -} - -.ace_fade-fold-widgets:hover .ace_fold-widget { - -webkit-transition: opacity 0.05s ease 0.05s; - transition: opacity 0.05s ease 0.05s; - opacity:1; -} - -.ace_underline { - text-decoration: underline; -} - -.ace_bold { - font-weight: bold; -} - -.ace_nobold .ace_bold { - font-weight: normal; -} - -.ace_italic { - font-style: italic; -} - - -.ace_error-marker { - background-color: rgba(255, 0, 0,0.2); - position: absolute; - z-index: 9; -} - -.ace_highlight-marker { - background-color: rgba(255, 255, 0,0.2); - position: absolute; - z-index: 8; -} diff --git a/app/styles/components/_ace.scss b/app/styles/components/_ace.scss index e9659ce..75423c6 100644 --- a/app/styles/components/_ace.scss +++ b/app/styles/components/_ace.scss @@ -1,12 +1,3 @@ - - -#javascript-editor { - position: relative; - height: 500px; - width: 620px; - margin: 0 auto; -} - #coffee-editor { left: 0; bottom: 0; @@ -24,6 +15,8 @@ } .ace_editor { + height: 95%; + float: left; position: relative; overflow: hidden; font: 12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; diff --git a/app/styles/components/_recording-actions.scss b/app/styles/components/_recording-actions.scss new file mode 100644 index 0000000..f684950 --- /dev/null +++ b/app/styles/components/_recording-actions.scss @@ -0,0 +1,69 @@ +.recording-actions { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + + .action-wrapper { + padding: .5em; + width: 250px; + margin: 0 auto; + border-radius: 3px; + text-align: center; + background: $graySlightBlue; + transition: transform 1.0s; + transform-style: preserve-3d; + } + + .progress { + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 16px; + -webkit-transform: rotateX(-90deg); + transform: rotateX(-90deg); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + background: $deepGrayBlue; + transition: all .5s; + } + + &[data-phase=started] { + perspective: 900px; + background: transparent; + } + + &[data-phase=started] .action-wrapper { + -webkit-transform: rotateX(75deg); + transform: rotateX(75deg); + } + + &[data-phase=stopped] .action-wrapper { + -webkit-transform: rotateX(0deg); + transform: rotateX(0deg); + } + +} + + +.icon-circle { + position: absolute; + left: .5em; + top: 1em; + font-size: 0.7em; + color: $inertRecording; +} +.recording > .icon-circle { + -webkit-animation: recordingPulse 2.0s linear infinite; + animation: recordingPulse 2s linear infinite; +} + +.record-btn, +.play-btn, +.stop-btn, +.pause-btn{ + cursor: pointer; + font-size: 1.5em; + color: $purpleAction; +} diff --git a/app/styles/fonts/_fontcustom.scss b/app/styles/fonts/_fontcustom.scss new file mode 100644 index 0000000..38ead0a --- /dev/null +++ b/app/styles/fonts/_fontcustom.scss @@ -0,0 +1,67 @@ +// +// Icon Font: fontcustom +// + +@font-face { + font-family: "fontcustom"; + src: url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.eot"); + src: url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.eot?#iefix") format("embedded-opentype"), + url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.woff") format("woff"), + url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.ttf") format("truetype"), + url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.svg#fontcustom") format("svg"); + font-weight: normal; + font-style: normal; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: "fontcustom"; + src: url("./fontcustom_9d31e57057239d5c84c067dc119bc27f.svg#fontcustom") format("svg"); + } +} + +[data-icon]:before { content: attr(data-icon); } + +[data-icon]:before, +.icon-circle:before, +.icon-line-pause:before, +.icon-line-play:before, +.icon-line-radio-microphone:before, +.icon-line-stop:before, +.icon-pause:before, +.icon-play:before, +.icon-radio-microphone:before, +.icon-stop:before { + display: inline-block; + font-family: "fontcustom"; + font-style: normal; + font-weight: normal; + font-variant: normal; + line-height: 1; + text-decoration: inherit; + text-rendering: optimizeLegibility; + text-transform: none; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.icon-circle:before { content: "\f108"; } +.icon-line-pause:before { content: "\f100"; } +.icon-line-play:before { content: "\f101"; } +.icon-line-radio-microphone:before { content: "\f102"; } +.icon-line-stop:before { content: "\f103"; } +.icon-pause:before { content: "\f104"; } +.icon-play:before { content: "\f105"; } +.icon-radio-microphone:before { content: "\f106"; } +.icon-stop:before { content: "\f107"; } + +$font-fontcustom-circle: "\f108"; +$font-fontcustom-line-pause: "\f100"; +$font-fontcustom-line-play: "\f101"; +$font-fontcustom-line-radio-microphone: "\f102"; +$font-fontcustom-line-stop: "\f103"; +$font-fontcustom-pause: "\f104"; +$font-fontcustom-play: "\f105"; +$font-fontcustom-radio-microphone: "\f106"; +$font-fontcustom-stop: "\f107"; diff --git a/app/styles/fonts/fontcustom-preview.html b/app/styles/fonts/fontcustom-preview.html new file mode 100644 index 0000000..32fb895 --- /dev/null +++ b/app/styles/fonts/fontcustom-preview.html @@ -0,0 +1,338 @@ + + + + fontcustom glyphs preview + + + + + + + + + +
+
+

fontcustom contains 9 glyphs:

+ Toggle Preview Characters +
+ + +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+ + + +
+ + diff --git a/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.eot b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.eot new file mode 100644 index 0000000..5eff37f Binary files /dev/null and b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.eot differ diff --git a/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.svg b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.svg new file mode 100644 index 0000000..de3c416 --- /dev/null +++ b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.svg @@ -0,0 +1,63 @@ + + + + + +Created by FontForge 20150302 at Thu Mar 19 00:11:12 2015 + By Adam Schuld +Copyright (c) 2015, Adam Schuld + + + + + + + + + + + + + + + + + diff --git a/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.ttf b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.ttf new file mode 100644 index 0000000..8d25507 Binary files /dev/null and b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.ttf differ diff --git a/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.woff b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.woff new file mode 100644 index 0000000..a065e55 Binary files /dev/null and b/app/styles/fonts/fontcustom_9d31e57057239d5c84c067dc119bc27f.woff differ diff --git a/app/styles/global/_animations.scss b/app/styles/global/_animations.scss new file mode 100644 index 0000000..131b527 --- /dev/null +++ b/app/styles/global/_animations.scss @@ -0,0 +1,39 @@ +@-webkit-keyframes recordingPulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + color: $baseRecordingRed; + } + + 50% { + -webkit-transform: scale3d(1.15, 1.15, 1.15); + transform: scale3d(1.15, 1.15, 1.15); + color: $growthRecordingRed; + } + + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + color: $baseRecordingRed; + } +} + +@keyframes recordingPulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + color: $baseRecordingRed; + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + color: $growthRecordingRed; + } + + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + color: $baseRecordingRed; + } +} diff --git a/app/styles/global/_fonts.scss b/app/styles/global/_fonts.scss deleted file mode 100644 index e69de29..0000000 diff --git a/app/styles/global/_variables.scss b/app/styles/global/_variables.scss new file mode 100644 index 0000000..40b8324 --- /dev/null +++ b/app/styles/global/_variables.scss @@ -0,0 +1,9 @@ +//Colors +$purpleAction: rgb(182, 109, 226); +$graySlightBlue: rgb(37, 38, 45); +$lighterGrayBlue: rgb(47, 48, 55); +$deepGrayBlue: rgb(28, 29, 36); +$inertRecording: rgb(65, 53, 57); + +$baseRecordingRed: rgba(237, 43, 108, 0.50); +$growthRecordingRed: rgba(237, 43, 108, 0.660); diff --git a/app/styles/layout/_layout.scss b/app/styles/layout/_layout.scss index 49bd235..33646c2 100644 --- a/app/styles/layout/_layout.scss +++ b/app/styles/layout/_layout.scss @@ -1,9 +1,13 @@ body { background: #31322F; + font-size: 16px; } // mounted div for app #app > div { - height: 95%; - position: fixed; + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; } diff --git a/app/styles/main.scss b/app/styles/main.scss index 07585bf..421c8f0 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -4,7 +4,9 @@ //--- Global // Global information that allow us to cascade information // Font, Color variables, etc -@import "global/fonts"; +@import "global/variables"; +@import "global/animations"; +@import "fonts/fontcustom"; //--- Layout // Abstract and related to the positioning of global elements @@ -19,6 +21,7 @@ // Shared pieces or blocks on pages // Forms, navigation @import "components/ace"; +@import "components/recording-actions"; //--- Partials // Unique differences on a per partial basis diff --git a/bower.json b/bower.json index 8bb94f0..1142b68 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,6 @@ "name": "followcodes", "version": "0.0.1", "dependencies": { - "jquery": "~2.1.1", "bootstrap-sass-official": ">=3.3.0", "modernizr": "^2.8.3" } diff --git a/gulpfile.js b/gulpfile.js index 5b571d8..bb506c4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -52,6 +52,12 @@ gulp.task('scripts', function () { }); +gulp.task('workers', function () { + gulp.src('app/scripts/async/_recorderWorker.js', {base: 'app/scripts'}) + .pipe(gulp.dest('dist/scripts/')); + +}); + gulp.task('json', function() { gulp.src('app/scripts/json/**/*.json', {base: 'app/scripts'}) .pipe(gulp.dest('dist/scripts/')); @@ -77,6 +83,11 @@ gulp.task('images', function () { .pipe($.size()); }); +gulp.task('fonts', function() { + gulp.src('app/styles/fonts/*') + .pipe(gulp.dest('dist/styles')); +}); + // Jest Testing gulp.task('jest', function () { var nodeModules = path.resolve('./node_modules'); @@ -94,7 +105,7 @@ gulp.task('clean', function (cb) { // Bundle -gulp.task('bundle', ['styles', 'scripts', 'bower'], function(){ +gulp.task('bundle', ['styles', 'fonts', 'scripts', 'workers', 'bower'], function(){ return gulp.src('./app/*.html') .pipe($.useref.assets()) .pipe($.useref.restore()) @@ -126,7 +137,7 @@ gulp.task('extras', function () { }); // Watch -gulp.task('watch', ['html', 'bundle', 'serve'], function () { +gulp.task('watch', ['html', 'bundle', 'jest'], function () { // 'serve' // Watch .json files gulp.watch('app/scripts/**/*.json', ['json']); // Watch .js files @@ -143,4 +154,7 @@ gulp.task('watch', ['html', 'bundle', 'serve'], function () { gulp.task('build', ['html', 'bundle', 'images', 'extras']); // Default task -gulp.task('default', ['clean', 'build', 'watch', 'serve' ]); //'jest', +gulp.task('default', ['clean', 'build', 'watch', 'jest' ]); // 'serve' + +// Test +gulp.task('test', ['jest' ]); diff --git a/mockups-designs/beginning.rp b/mockups-designs/beginning.rp new file mode 100644 index 0000000..842414f Binary files /dev/null and b/mockups-designs/beginning.rp differ diff --git a/package.json b/package.json index 3c0f662..49f001b 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,13 @@ "cookie-parser": "^1.3.4", "ejs": "^2.3.1", "es6-promise": "^2.0.1", + "es6-promise-polyfill": "^1.0.0", + "events": "^1.0.2", "express": "^4.12.2", "lodash": "^3.3.1", "path": "^0.11.14", - "react": "latest" + "react": "latest", + "react-tools": "^0.13.1" }, "devDependencies": { "browserify-shim": "^3.8.0", @@ -29,6 +32,7 @@ "gulp-util": "~3.0.1", "gulp-webserver": "latest", "jest": "latest", + "jest-cli": "^0.4.0", "node-jsx": "^0.12.4", "reactify": "latest", "vinyl-source-stream": "^1.0.0", diff --git a/sandbox/ProgressButtonStyles/button.html b/sandbox/ProgressButtonStyles/button.html new file mode 100644 index 0000000..8e6e67d --- /dev/null +++ b/sandbox/ProgressButtonStyles/button.html @@ -0,0 +1,156 @@ + + + + + + + Progress Button Styles + + + + + + + + + + + + + +
+ + +
+

Progress Button Styles Creative effects for loading buttons

+

Without support for transform-style: preserve-3d you will see the fallback style (fill horizontal).

+
+
+
+

fill horizontal

+ +
+
+

fill vertical

+ +
+
+

shrink horizontal

+ +
+
+

shrink vertical

+ +
+
+
+
+

rotate-angle-bottom
perspective

+ +
+
+

rotate-angle-top
perspective

+ +
+
+

rotate-angle-left
perspective

+ +
+
+

rotate-angle-right
perspective

+ +
+
+
+
+

rotate-side-down
perspective

+ +
+
+

rotate-side-up
perspective

+ +
+
+

rotate-side-left
perspective

+ +
+
+

rotate-side-right
perspective

+ +
+
+
+
+

rotate-back
perspective

+ +
+
+

flip-open
perspective

+ +
+
+

slide-down
horizontal

+ +
+
+

move-up
horizontal

+ +
+
+
+
+

top-line
horizontal

+ +
+
+

lateral-lines
vertical

+ +
+ +
+ +
+ + + + + + +
 BuySellAds
Carbon Ads
Google Analytics
diff --git a/sandbox/ProgressButtonStyles/js/5933-17f65e6063df008f3c413fea2ab9659188658b08-bsa.jpg b/sandbox/ProgressButtonStyles/js/5933-17f65e6063df008f3c413fea2ab9659188658b08-bsa.jpg new file mode 100644 index 0000000..9d0ba30 Binary files /dev/null and b/sandbox/ProgressButtonStyles/js/5933-17f65e6063df008f3c413fea2ab9659188658b08-bsa.jpg differ diff --git a/sandbox/ProgressButtonStyles/js/C6AILKT.json b/sandbox/ProgressButtonStyles/js/C6AILKT.json new file mode 100644 index 0000000..dd90c0e --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/C6AILKT.json @@ -0,0 +1 @@ +_carbonads_go({"ads":[{"startingdate":"1405600200","org_zoneid":"1673","marketplace_zoneid":"1297933","styles":"","org_bannerid":"11696","statimp":"http://srv.buysellads.com/ads/imp/x/GTND423LCYAD6537CWY4YKQWCVSDEK7JCV7DVZ3JCE7DK53LCTSIPKQKC6BD62JYCVSIKK3EHJWNBADLKM7UCBZG2Y","zonekey":"C6AILKT","marketplace_siteid":"246311","org_creativeid":"15434","bannerid":"7720","html":"{\"fetch\":\"//adn.fusionads.net/launchbit/9533/developers/\"}","i":"1426830214","showbsalink":"0","zoneid":"1990","endingdate":"0","customcriterion":"","timestamp":"1426830214","width":"130","extendo_0":"1","evenodd":"0","statlink_default":"http://srv.buysellads.com/ads/click/x/GTND423LCYAD6537CWY4YKQWCVSDEK7JCV7DVZ3JCE7DK53LCTSIPKQKC6BD62JYCVSIKK3EHJNCLSIZZRLCP7I35MNFV","creativeid":"10207","link_default":"http://pro.buysellads.com/","buynow_url":"http://buysellads.com/buy/detail/246311/zone/1297933?utm_source=site_246311&utm_medium=website&utm_campaign=imagetext&utm_content=zone_1297933","height":"100","active":1},{}]}); \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/carbon.js b/sandbox/ProgressButtonStyles/js/carbon.js new file mode 100644 index 0000000..1967be0 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/carbon.js @@ -0,0 +1 @@ +var _carbonads={init:function(){var placement=this.getUrlVar('placement');var circle=this.getUrlVar('circle');var serve=this.getUrlVar('serve');var baseurl='srv.buysellads.com';if(window.location.protocol!='https:')baseurl='srv.carbonads.net';var pro=document.createElement('script');pro.id='_carbonads_projs';pro.type='text/javascript';pro.src='//'+baseurl+'/ads/'+serve+'.json?segment=placement:'+placement+'&callback=_carbonads_go';var ck=document.cookie,day=ck.indexOf('_bsap_daycap='),life=ck.indexOf('_bsap_lifecap=');day=day>=0?ck.substring(day+12+1).split(';')[0].split(','):[];life=life>=0?ck.substring(life+13+1).split(';')[0].split(','):[];if(day.length||life.length){var freqcap=[];for(var i=0;i0)fulllink=link[0]+'?segment=placement:'+placement+';';else fulllink=link[0];if(window.location.protocol!='https:')fulllink=fulllink.replace('srv.buysellads.com','srv.carbonads.net');var el=document.createElement('span');el.innerHTML=''+ad.description+'ads via Carbon';if(typeof ad.pixel!='undefined'){var pix=document.createElement('img');pix.src=ad.pixel;pix.border='0';pix.height='1';pix.width='1';pix.style.display='none';el.appendChild(pix)}var fdiv=document.createElement('div');fdiv.id='carbonads';fdiv.appendChild(el);var carbonjs=document.getElementById('_carbonads_js');if(carbonjs!=null)carbonjs.parentNode.insertBefore(fdiv,carbonjs.nextSibling);_bsap_serving_callback(ad.bannerid,ad.zonekey,ad.freqcap)}_carbonads.init();window['_bsap_serving_callback']=function(banner,zone,freqcap){var append=function(w,data,days){var c=document.cookie,i=c.indexOf(w+'='),existing=i>=0?c.substring(i+w.length+1).split(';')[0]+',':'',d=new Date();d.setTime(days*3600000+d);data=existing+data;data=data.substring(0,2048);document.cookie=w+'='+data+'; expires='+d.toGMTString()+'; path=\/'};if(freqcap){append('_bsap_daycap',banner,1);append('_bsap_lifecap',banner,365)}}; \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/classie.js b/sandbox/ProgressButtonStyles/js/classie.js new file mode 100644 index 0000000..a967554 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/classie.js @@ -0,0 +1,80 @@ +/*! + * classie - class helper functions + * from bonzo https://github.com/ded/bonzo + * + * classie.has( elem, 'my-class' ) -> true/false + * classie.add( elem, 'my-new-class' ) + * classie.remove( elem, 'my-unwanted-class' ) + * classie.toggle( elem, 'my-class' ) + */ + +/*jshint browser: true, strict: true, undef: true */ +/*global define: false */ + +( function( window ) { + +'use strict'; + +// class helper functions from bonzo https://github.com/ded/bonzo + +function classReg( className ) { + return new RegExp("(^|\\s+)" + className + "(\\s+|$)"); +} + +// classList support for class management +// altho to be fair, the api sucks because it won't accept multiple classes at once +var hasClass, addClass, removeClass; + +if ( 'classList' in document.documentElement ) { + hasClass = function( elem, c ) { + return elem.classList.contains( c ); + }; + addClass = function( elem, c ) { + elem.classList.add( c ); + }; + removeClass = function( elem, c ) { + elem.classList.remove( c ); + }; +} +else { + hasClass = function( elem, c ) { + return classReg( c ).test( elem.className ); + }; + addClass = function( elem, c ) { + if ( !hasClass( elem, c ) ) { + elem.className = elem.className + ' ' + c; + } + }; + removeClass = function( elem, c ) { + elem.className = elem.className.replace( classReg( c ), ' ' ); + }; +} + +function toggleClass( elem, c ) { + var fn = hasClass( elem, c ) ? removeClass : addClass; + fn( elem, c ); +} + +var classie = { + // full names + hasClass: hasClass, + addClass: addClass, + removeClass: removeClass, + toggleClass: toggleClass, + // short names + has: hasClass, + add: addClass, + remove: removeClass, + toggle: toggleClass +}; + +// transport +if ( typeof define === 'function' && define.amd ) { + // AMD + define( classie ); +} else { + // browser global + window.classie = classie; +} + +})( window ); diff --git a/sandbox/ProgressButtonStyles/js/component.css b/sandbox/ProgressButtonStyles/js/component.css new file mode 100644 index 0000000..38c119e --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/component.css @@ -0,0 +1,589 @@ +*, *:after, *::before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +@font-face { + font-weight: normal; + font-style: normal; + font-family: 'icomoon'; + src:url('../fonts/icomoon/icomoon.eot'); + src:url('../fonts/icomoon/icomoon.eot?#iefix') format('embedded-opentype'), + url('../fonts/icomoon/icomoon.ttf') format('truetype'), + url('../fonts/icomoon/icomoon.woff') format('woff'), + url('../fonts/icomoon/icomoon.svg#icomoon') format('svg'); +} + +/* General styles for all types of buttons */ +.progress-button { + position: relative; + display: inline-block; + padding: 0 60px; + outline: none; + border: none; + background: #1d9650; + color: #fff; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 1em; + line-height: 4; +} + +.progress-button[disabled], +.progress-button[disabled].state-loading { + cursor: default; +} + +.progress-button .content { + position: relative; + display: block; +} + +.progress-button .content::before, +.progress-button .content::after { + position: absolute; + right: 20px; + color: #0e7138; + font-family: "icomoon"; + opacity: 0; + -webkit-transition: opacity 0.3s 0.3s; + transition: opacity 0.3s 0.3s; +} + +.progress-button .content::before { + content: "\e600"; /* Checkmark for success */ +} + +.progress-button .content::after { + content: "\e601"; /* Cross for error */ +} + +.progress-button.state-success .content::before, +.progress-button.state-error .content::after { + opacity: 1; +} + +.notransition { + -webkit-transition: none !important; + transition: none !important; +} + +.progress-button .progress { + background: #148544; +} + +.progress-button .progress-inner { + position: absolute; + left: 0; + background: #0e7138; +} + +.progress-button[data-horizontal] .progress-inner { + top: 0; + width: 0; + height: 100%; + -webkit-transition: width 0.3s, opacity 0.3s; + transition: width 0.3s, opacity 0.3s; +} + +.progress-button[data-vertical] .progress-inner { + bottom: 0; + width: 100%; + height: 0; + -webkit-transition: height 0.3s, opacity 0.3s; + transition: height 0.3s, opacity 0.3s; +} + +/* Necessary 3d styles for buttons with perspective */ + +.progress-button[data-perspective] { + position: relative; + display: inline-block; + padding: 0; + background: transparent; + -webkit-perspective: 900px; + perspective: 900px; +} + +.progress-button[data-perspective] .content { + padding: 0 60px; + background: #1d9650; +} + +.progress-button[data-perspective] .progress-wrap { + display: block; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; +} + +.progress-button[data-perspective] .content, +.progress-button[data-perspective] .progress { + outline: 1px solid rgba(0,0,0,0); /* Smoothen jagged edges in FF */ +} + +/* Individual styles */ +/* Choose the effect(s) you want, delete the rest */ + +/* Fill horizontal */ +/* ====================== */ + +.progress-button[data-style="fill"][data-horizontal] { + overflow: hidden; +} + +.progress-button[data-style="fill"][data-horizontal] .content { + z-index: 10; + -webkit-transition: -webkit-transform 0.3s; + transition: transform 0.3s; +} + +.progress-button[data-style="fill"][data-horizontal] .content::before, +.progress-button[data-style="fill"][data-horizontal] .content::after { + top: 100%; + right: auto; + left: 50%; + -webkit-transition: opacity 0.3s; + transition: opacity 0.3s; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.progress-button[data-style="fill"][data-horizontal].state-success .content, +.progress-button[data-style="fill"][data-horizontal].state-error .content { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); +} + +/* Fill vertical */ +/* ====================== */ + +.progress-button[data-style="fill"][data-vertical] { + overflow: hidden; +} + +.progress-button[data-style="fill"][data-vertical] .content { + z-index: 10; + -webkit-transition: -webkit-transform 0.3s; + transition: transform 0.3s; +} + +.progress-button[data-style="fill"][data-vertical] .content::before, +.progress-button[data-style="fill"][data-vertical] .content::after { + top: 100%; + right: auto; + left: 50%; + -webkit-transition: opacity 0.3s; + transition: opacity 0.3s; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.progress-button[data-style="fill"][data-vertical].state-success .content, +.progress-button[data-style="fill"][data-vertical].state-error .content { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); +} + +/* Shrink horizontal */ +/* ====================== */ + +.progress-button[data-style="shrink"] { /* common for horizontal and vertical */ + overflow: hidden; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.progress-button[data-style="shrink"][data-horizontal] .content { + -webkit-transition: opacity 0.3s, -webkit-transform 0.3s; + transition: opacity 0.3s, transform 0.3s; +} + +.progress-button[data-style="shrink"][data-horizontal] .content::before, +.progress-button[data-style="shrink"][data-horizontal] .content::after { + top: 100%; + right: auto; + left: 50%; + -webkit-transition: opacity 0.3s; + transition: opacity 0.3s; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.progress-button[data-style="shrink"][data-horizontal].state-loading { + -webkit-transform: scaleY(0.3); + transform: scaleY(0.3); +} + +.progress-button[data-style="shrink"][data-horizontal].state-loading .content { + opacity: 0; +} + +.progress-button[data-style="shrink"][data-horizontal].state-success .content, +.progress-button[data-style="shrink"][data-horizontal].state-error .content { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); +} + +/* Shrink vertical */ +/* ====================== */ + +.progress-button[data-style="shrink"][data-vertical] .content { + -webkit-transition: opacity 0.3s, -webkit-transform 0.3s; + transition: opacity 0.3s, transform 0.3s; +} + +.progress-button[data-style="shrink"][data-vertical] .content::before, +.progress-button[data-style="shrink"][data-vertical] .content::after { + top: 100%; + right: auto; + left: 50%; + -webkit-transition: opacity 0.3s; + transition: opacity 0.3s; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.progress-button[data-style="shrink"][data-vertical].state-loading { + -webkit-transform: scaleX(0.1); + transform: scaleX(0.1); +} + +.progress-button[data-style="shrink"][data-vertical].state-loading .content { + opacity: 0; +} + +.progress-button[data-style="shrink"][data-vertical].state-success .content, +.progress-button[data-style="shrink"][data-vertical].state-error .content { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); +} + +/* Rotate bottom 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-angle-bottom"] .progress { + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 20px; + box-shadow: 0 -1px 0 #148544; /* fix the blurriness that causes a gap */ + -webkit-transform: rotateX(-90deg); + transform: rotateX(-90deg); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; +} + +.progress-button[data-style="rotate-angle-bottom"].state-loading .progress-wrap { + -webkit-transform: rotateX(45deg); + transform: rotateX(45deg); +} + +/* Rotate top 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-angle-top"] .progress { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + height: 20px; + box-shadow: 0 1px 0 #148544; /* fix the blurriness that causes a gap */ + -webkit-transform: rotateX(90deg); + transform: rotateX(90deg); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; +} + +.progress-button[data-style="rotate-angle-top"].state-loading .progress-wrap { + -webkit-transform: rotateX(-45deg); + transform: rotateX(-45deg); +} + +/* Rotate left 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-angle-left"] .progress { + position: absolute; + top: 0; + right: 100%; + width: 20px; + height: 100%; + box-shadow: 1px 0 0 #148544; /* fix the blurriness that causes a gap */ + -webkit-transform: rotateY(-90deg); + transform: rotateY(-90deg); + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; +} + +.progress-button[data-style="rotate-angle-left"].state-loading .progress-wrap { + -webkit-transform: rotateY(45deg); + transform: rotateY(45deg); +} + +/* Rotate right 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-angle-right"] .progress { + position: absolute; + top: 0; + left: 100%; + width: 20px; + height: 100%; + box-shadow: -1px 0 0 #148544; /* fix the blurriness that causes a gap */ + -webkit-transform: rotateY(90deg); + transform: rotateY(90deg); + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; +} + +.progress-button[data-style="rotate-angle-right"].state-loading .progress-wrap { + -webkit-transform: rotateY(-45deg); + transform: rotateY(-45deg); +} + +/* Rotate side down 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-side-down"] .progress { + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 20px; + -webkit-transform: rotateX(-90deg); + transform: rotateX(-90deg); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="rotate-side-down"].state-loading .progress-wrap { + -webkit-transform: rotateX(90deg) translateZ(10px); + transform: rotateX(90deg) translateZ(10px); +} + +/* Rotate side up 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-side-up"] .progress { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + height: 20px; + -webkit-transform: rotateX(90deg); + transform: rotateX(90deg); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="rotate-side-up"].state-loading .progress-wrap { + -webkit-transform: rotateX(-90deg) translateZ(10px); + transform: rotateX(-90deg) translateZ(10px); +} + +/* Rotate side left 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-side-left"] .progress-wrap { + -webkit-transform-origin: 0 50%; + transform-origin: 0 50%; +} + +.progress-button[data-style="rotate-side-left"] .progress { + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 100%; + -webkit-transform: rotateY(90deg); + transform: rotateY(90deg); + -webkit-transform-origin: 0 50%; + transform-origin: 0 50%; +} + +.progress-button[data-style="rotate-side-left"].state-loading .progress-wrap { + -webkit-transform: translateX(50%) rotateY(90deg) translateZ(10px); + transform: translateX(50%) rotateY(90deg) translateZ(10px); +} + +/* Rotate side right 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-side-right"] .progress-wrap { + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; +} + +.progress-button[data-style="rotate-side-right"] .progress { + position: absolute; + top: 0; + left: 100%; + width: 20px; + height: 100%; + -webkit-transform: rotateY(90deg); + transform: rotateY(90deg); + -webkit-transform-origin: 0 50%; + transform-origin: 0 50%; +} + +.progress-button[data-style="rotate-side-right"].state-loading .progress-wrap { + -webkit-transform: translateX(-50%) rotateY(-90deg) translateZ(10px); + transform: translateX(-50%) rotateY(-90deg) translateZ(10px); +} + +/* Rotate back 3d */ +/* ====================== */ + +.progress-button[data-style="rotate-back"] .progress-wrap { + -webkit-transition-timing-function: ease-out; + transition-timing-function: ease-out; +} + +.progress-button[data-style="rotate-back"] .content { + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="rotate-back"] .progress { + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 100%; + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="rotate-back"].state-loading .progress-wrap { + -webkit-transform: rotateX(180deg) scaleX(0.6) scaleY(0.3); + transform: rotateX(180deg) scaleX(0.6) scaleY(0.3); +} + +/* flip open 3d */ +/* ====================== */ + +.progress-button[data-style="flip-open"] .content { + z-index: 10; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; + -webkit-transform-origin: 50% 0; + transform-origin: 50% 0; +} + +.progress-button[data-style="flip-open"] .progress { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.progress-button[data-style="flip-open"].state-loading .content { + -webkit-transform: rotateX(45deg); + transform: rotateX(45deg); +} + +/* slide down */ +/* ====================== */ + +.progress-button[data-style="slide-down"] { + padding: 0; + overflow: visible; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="slide-down"] .content { + z-index: 10; + padding: 0 60px; + background: #1d9650; +} + +.progress-button[data-style="slide-down"] .progress { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.progress-button[data-style="slide-down"].state-loading .progress { + -webkit-transform: translateY(10px); + transform: translateY(10px); +} + +/* move-up */ +/* ====================== */ + +.progress-button[data-style="move-up"] { + padding: 0; + overflow: visible; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.progress-button[data-style="move-up"] .content { + z-index: 10; + padding: 0 60px; + background: #1d9650; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.progress-button[data-style="move-up"] .progress { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.progress-button[data-style="move-up"].state-loading .content { + -webkit-transform: translateY(-10px); + transform: translateY(-10px); +} + +/* top-line */ +/* ====================== */ + +.progress-button[data-style="top-line"] .progress-inner { + height: 3px; +} + +.progress-button[data-style="top-line"] .content::before, +.progress-button[data-style="top-line"] .content::after { + right: auto; + left: 100%; + margin-left: 25px; +} + +/* lateral-lines */ +/* ====================== */ + +.progress-button[data-style="lateral-lines"] .progress-inner { + width: 100%; + border-right: 3px solid #0e7138; + border-left: 3px solid #0e7138; + background: transparent; +} + +.progress-button[data-style="lateral-lines"] .content::before, +.progress-button[data-style="lateral-lines"] .content::after { + right: auto; + left: 100%; + margin-left: 25px; +} \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/demo.css b/sandbox/ProgressButtonStyles/js/demo.css new file mode 100644 index 0000000..d80f410 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/demo.css @@ -0,0 +1,154 @@ +@import url(http://fonts.googleapis.com/css?family=Lato:300,400,700); +@font-face { + font-family: 'codropsicons'; + src:url('../fonts/codropsicons/codropsicons.eot'); + src:url('../fonts/codropsicons/codropsicons.eot?#iefix') format('embedded-opentype'), + url('../fonts/codropsicons/codropsicons.woff') format('woff'), + url('../fonts/codropsicons/codropsicons.ttf') format('truetype'), + url('../fonts/codropsicons/codropsicons.svg#codropsicons') format('svg'); + font-weight: normal; + font-style: normal; +} + +body { + background: #27ae60; + color: #fff; + font-family: 'Lato', Arial, sans-serif; +} + +.clearfix:before, +.clearfix:after { + content: ''; + display: table; +} + +.clearfix:after { + clear: both; +} + +a { + color: #148544; + text-decoration: none; + outline: none; +} + +a:hover, a:focus { + color: #fff; +} + +.container > header { + margin: 0 auto 2em; + padding: 2em; + text-align: center; + background: rgba(0,0,0,0.01); +} + +.container > header h1 { + font-size: 3.5em; + line-height: 1.3; + margin: 0; + font-weight: 300; +} + +.container > header span { + display: block; + font-size: 60%; + color: #148544; + padding: 0.3em 0 0.6em 0.1em; +} + +.note { + color: #85cd62; +} + +/* To Navigation Style */ +.codrops-top { + text-transform: uppercase; + width: 100%; + font-size: 0.69em; + line-height: 2.2; + font-weight: 700; +} + +.codrops-top a { + text-decoration: none; + padding: 0 1em; + letter-spacing: 0.1em; + display: inline-block; +} + +.codrops-top span.right { + float: right; +} + +.codrops-top span.right a { + float: left; + display: block; +} + +.codrops-icon:before { + font-family: 'codropsicons'; + margin: 0 4px; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} + +.codrops-icon-drop:before { + content: "\e001"; +} + +.codrops-icon-prev:before { + content: "\e004"; +} + +.wrapper { + text-align: center; + vertical-align: top; +} + +section { + position: relative; + text-align: center; + padding: 1em 1em 3em; +} + +.wrapper section { + display: inline-block; + width: 300px; +} + +section h2 { + text-transform: uppercase; + letter-spacing: 1px; + color: #148544; + font-weight: 700; + padding: 1em; + font-size: 0.8em; + -webkit-backface-visibility: hidden; +} + +.related { + font-size: 2em; + font-weight: 300; +} + +@media screen and (max-width: 25em) { + + .container > header { + font-size: 70%; + } + + .related { + font-size: 120%; + } + + .codrops-icon span { + display: none; + } + +} \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/demoad.js b/sandbox/ProgressButtonStyles/js/demoad.js new file mode 100644 index 0000000..cadfd56 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/demoad.js @@ -0,0 +1,24 @@ +var filename='http://tympanus.net/codrops/adpacks/demoadpacks.css?' + new Date().getTime(); +var fileref=document.createElement("link"); +fileref.setAttribute("rel", "stylesheet"); +fileref.setAttribute("type", "text/css"); +fileref.setAttribute("href", filename); +document.getElementsByTagName("head")[0].appendChild(fileref); + +var demoad = document.createElement('div'); +demoad.id = 'cdawrap'; +demoad.innerHTML = ''; +document.getElementsByTagName('body')[0].appendChild(demoad); + +document.getElementById('cda-remove').addEventListener('click',function(e){ + demoad.style.display = 'none'; + e.preventDefault(); +}); + +var bsa = document.createElement('script'); +bsa.type = 'text/javascript'; +bsa.async = true; +bsa.id = '_carbonads_js'; +bsa.src = '//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=codrops'; +demoad.appendChild(bsa); + diff --git a/sandbox/ProgressButtonStyles/js/demoadpacks.css b/sandbox/ProgressButtonStyles/js/demoadpacks.css new file mode 100644 index 0000000..c7a0ff3 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/demoadpacks.css @@ -0,0 +1,108 @@ +#cdawrap { + text-align: center; + width: 178px; + height: auto; + right: 15px; + top: 35px; + position: fixed; + background: rgba(255,255,255,0.8); + border: 1px solid rgba(0,0,0,0.05); + z-index: 1000000; + font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + padding: 24px 24px 10px 24px; + font-size: 12px; + font-weight: 700; +} + +#cdawrap .carbon-img { + display: block; + text-align: center; + border: none; +} + +#cdawrap .carbon-text { + color: #4f5152; + padding: 10px 0 15px 0; + display: block; + line-height: 1.2; + text-decoration: none; +} + +#cdawrap a.carbon-poweredby { + display: block; + text-transform: uppercase; + font-size: 65%; + letter-spacing: 1px; + color: #cc301e; + font-weight: 700; + text-decoration: none; +} + +#cdawrap a.carbon-poweredby:hover, +#cdawrap a.carbon-poweredby:focus { + color: #333; +} + +#cda-remove { + width: 24px; + height: 24px; + position: absolute; + top: 0; + right: 0; + line-height: 1; + text-align: center; + cursor: pointer; + font-size: 12px; + line-height: 24px; + color: #777; +} + +#cda-remove:hover { + background: rgba(0,0,0,0.1); +} + +#cda-remove:before { + content: 'x'; + display: inline-block; + font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif; +} + +@media screen and (max-width: 800px){ + #cdawrap { + width: 100%; + height: auto; + right: 0px; + top: auto !important; + bottom: 0px !important; + left: 0px !important; + background: #fff; + padding: 15px 50px 10px 10px; + text-align: left; + font-size: 11px; + } + + #cdawrap::before { + content: 'Advertisement'; + position: absolute; + top: 0; + left: 0; + color: #999; + font-size: 9px; + padding: 10px; + } + + #cdawrap .carbon-img { + display: none; + } + + #cda-remove { + width: 30px; + height: 30px; + font-size: 16px; + line-height: 30px; + } +} + +@media screen and (max-width: 460px){ + #cdawrap { display: none; } +} diff --git a/sandbox/ProgressButtonStyles/js/ga.js b/sandbox/ProgressButtonStyles/js/ga.js new file mode 100644 index 0000000..86cc52d --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/ga.js @@ -0,0 +1,66 @@ +(function(){var aa=encodeURIComponent,ba=Infinity,ca=setTimeout,da=isNaN,m=Math,ea=decodeURIComponent;function Ie(a,b){return a.onload=b}function Je(a,b){return a.onerror=b}function ha(a,b){return a.name=b} +var p="replace",la="floor",ma="charAt",oa="match",pa="port",qa="createElement",g="getTime",u="host",v="toString",y="split",ta="stopPropagation",z="location",va="search",A="protocol",xa="href",ya="apply",n="push",ia="test",ja="slice",ka="load",na="value",q="indexOf",ra="path",r="name",w="length",x="prototype",sa="clientWidth",ua="scope",wa="clientHeight",B="substring",za="navigator",C="join",D="toLowerCase",E;function Aa(a,b){switch(b){case 0:return""+a;case 1:return 1*a;case 2:return!!a;case 3:return 1E3*a}return a}function Ba(a){return"function"==typeof a}function Ca(a){return void 0!=a&&-1<(a.constructor+"")[q]("String")}function F(a,b){return void 0==a||"-"==a&&!b||""==a}function Da(a){if(!a||""==a)return"";for(;a&&-1<" \n\r\t"[q](a[ma](0));)a=a[B](1);for(;a&&-1<" \n\r\t"[q](a[ma](a[w]-1));)a=a[B](0,a[w]-1);return a}function Ea(){return m.round(2147483647*m.random())}function Fa(){} +function G(a,b){if(aa instanceof Function)return b?encodeURI(a):aa(a);H(68);return escape(a)}function I(a){a=a[y]("+")[C](" ");if(ea instanceof Function)try{return ea(a)}catch(b){H(17)}else H(68);return unescape(a)}var Ga=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}; +function Ia(a,b){if(a){var c=J[qa]("script");c.type="text/javascript";c.async=!0;c.src=a;c.id=b;var d=J.getElementsByTagName("script")[0];d.parentNode.insertBefore(c,d);return c}}function K(a){return a&&0a[y]("/")[0][q](":")&&(a=k+f[2][B](0,f[2].lastIndexOf("/"))+"/"+a):a=k+f[2]+(a||Be);d.href=a;e=c(d);return{protocol:(d[A]||"")[D](),host:e[0], +port:e[1],path:e[2],Oa:d[va]||"",url:a||""}}function Na(a,b){function c(b,c){a.contains(b)||a.set(b,[]);a.get(b)[n](c)}for(var d=Da(b)[y]("&"),e=0;ef?c(d[e],"1"):c(d[e][B](0,f),d[e][B](f+1))}}function Pa(a,b){if(F(a)||"["==a[ma](0)&&"]"==a[ma](a[w]-1))return"-";var c=J.domain;return a[q](c+(b&&"/"!=b?b:""))==(0==a[q]("http://")?7:0==a[q]("https://")?8:0)?"0":a};var Qa=0;function Ra(a,b,c){1<=Qa||1<=100*m.random()||ld()||(a=["utmt=error","utmerr="+a,"utmwv=5.6.3","utmn="+Ea(),"utmsp=1"],b&&a[n]("api="+b),c&&a[n]("msg="+G(c[B](0,100))),M.w&&a[n]("aip=1"),Sa(a[C]("&")),Qa++)};var Ta=0,Ua={};function N(a){return Va("x"+Ta++,a)}function Va(a,b){Ua[a]=!!b;return a} +var Wa=N(),Xa=Va("anonymizeIp"),Ya=N(),$a=N(),ab=N(),bb=N(),O=N(),P=N(),cb=N(),db=N(),eb=N(),fb=N(),gb=N(),hb=N(),ib=N(),jb=N(),kb=N(),lb=N(),nb=N(),ob=N(),pb=N(),qb=N(),rb=N(),sb=N(),tb=N(),ub=N(),vb=N(),wb=N(),xb=N(),yb=N(),zb=N(),Ab=N(),Bb=N(),Cb=N(),Db=N(),Eb=N(),Fb=N(!0),Gb=Va("currencyCode"),Hb=Va("page"),Ib=Va("title"),Jb=N(),Kb=N(),Lb=N(),Mb=N(),Nb=N(),Ob=N(),Pb=N(),Qb=N(),Rb=N(),Q=N(!0),Sb=N(!0),Tb=N(!0),Ub=N(!0),Vb=N(!0),Wb=N(!0),Zb=N(!0),$b=N(!0),ac=N(!0),bc=N(!0),cc=N(!0),R=N(!0),dc=N(!0), +ec=N(!0),fc=N(!0),gc=N(!0),hc=N(!0),ic=N(!0),jc=N(!0),S=N(!0),kc=N(!0),lc=N(!0),mc=N(!0),nc=N(!0),oc=N(!0),pc=N(!0),qc=N(!0),rc=Va("campaignParams"),sc=N(),tc=Va("hitCallback"),uc=N();N();var vc=N(),wc=N(),xc=N(),yc=N(),zc=N(),Ac=N(),Bc=N(),Cc=N(),Dc=N(),Ec=N(),Fc=N(),Gc=N(),Hc=N(),Ic=N();N();var Mc=N(),Nc=N(),Yb=N(),Jc=N(),Kc=N(),Lc=Va("utmtCookieName"),Cd=Va("displayFeatures"),Oc=N(),of=Va("gtmid"),Oe=Va("uaName"),Pe=Va("uaDomain"),Qe=Va("uaPath"),pf=Va("linkid");var Re=function(){function a(a,c,d){T(qf[x],a,c,d)}a("_createTracker",qf[x].r,55);a("_getTracker",qf[x].oa,0);a("_getTrackerByName",qf[x].u,51);a("_getTrackers",qf[x].pa,130);a("_anonymizeIp",qf[x].aa,16);a("_forceSSL",qf[x].la,125);a("_getPlugin",Pc,120)},Se=function(){function a(a,c,d){T(U[x],a,c,d)}Qc("_getName",$a,58);Qc("_getAccount",Wa,64);Qc("_visitCode",Q,54);Qc("_getClientInfo",ib,53,1);Qc("_getDetectTitle",lb,56,1);Qc("_getDetectFlash",jb,65,1);Qc("_getLocalGifPath",wb,57);Qc("_getServiceMode", +xb,59);V("_setClientInfo",ib,66,2);V("_setAccount",Wa,3);V("_setNamespace",Ya,48);V("_setAllowLinker",fb,11,2);V("_setDetectFlash",jb,61,2);V("_setDetectTitle",lb,62,2);V("_setLocalGifPath",wb,46,0);V("_setLocalServerMode",xb,92,void 0,0);V("_setRemoteServerMode",xb,63,void 0,1);V("_setLocalRemoteServerMode",xb,47,void 0,2);V("_setSampleRate",vb,45,1);V("_setCampaignTrack",kb,36,2);V("_setAllowAnchor",gb,7,2);V("_setCampNameKey",ob,41);V("_setCampContentKey",tb,38);V("_setCampIdKey",nb,39);V("_setCampMediumKey", +rb,40);V("_setCampNOKey",ub,42);V("_setCampSourceKey",qb,43);V("_setCampTermKey",sb,44);V("_setCampCIdKey",pb,37);V("_setCookiePath",P,9,0);V("_setMaxCustomVariables",yb,0,1);V("_setVisitorCookieTimeout",cb,28,1);V("_setSessionCookieTimeout",db,26,1);V("_setCampaignCookieTimeout",eb,29,1);V("_setReferrerOverride",Jb,49);V("_setSiteSpeedSampleRate",Dc,132);a("_trackPageview",U[x].Fa,1);a("_trackEvent",U[x].F,4);a("_trackPageLoadTime",U[x].Ea,100);a("_trackSocial",U[x].Ga,104);a("_trackTrans",U[x].Ia, +18);a("_sendXEvent",U[x].t,78);a("_createEventTracker",U[x].ia,74);a("_getVersion",U[x].qa,60);a("_setDomainName",U[x].B,6);a("_setAllowHash",U[x].va,8);a("_getLinkerUrl",U[x].na,52);a("_link",U[x].link,101);a("_linkByPost",U[x].ua,102);a("_setTrans",U[x].za,20);a("_addTrans",U[x].$,21);a("_addItem",U[x].Y,19);a("_clearTrans",U[x].ea,105);a("_setTransactionDelim",U[x].Aa,82);a("_setCustomVar",U[x].wa,10);a("_deleteCustomVar",U[x].ka,35);a("_getVisitorCustomVar",U[x].ra,50);a("_setXKey",U[x].Ca,83); +a("_setXValue",U[x].Da,84);a("_getXKey",U[x].sa,76);a("_getXValue",U[x].ta,77);a("_clearXKey",U[x].fa,72);a("_clearXValue",U[x].ga,73);a("_createXObj",U[x].ja,75);a("_addIgnoredOrganic",U[x].W,15);a("_clearIgnoredOrganic",U[x].ba,97);a("_addIgnoredRef",U[x].X,31);a("_clearIgnoredRef",U[x].ca,32);a("_addOrganic",U[x].Z,14);a("_clearOrganic",U[x].da,70);a("_cookiePathCopy",U[x].ha,30);a("_get",U[x].ma,106);a("_set",U[x].xa,107);a("_addEventListener",U[x].addEventListener,108);a("_removeEventListener", +U[x].removeEventListener,109);a("_addDevId",U[x].V);a("_getPlugin",Pc,122);a("_setPageGroup",U[x].ya,126);a("_trackTiming",U[x].Ha,124);a("_initData",U[x].v,2);a("_setVar",U[x].Ba,22);V("_setSessionTimeout",db,27,3);V("_setCookieTimeout",eb,25,3);V("_setCookiePersistence",cb,24,1);a("_setAutoTrackOutbound",Fa,79);a("_setTrackOutboundSubdomains",Fa,81);a("_setHrefExamineLimit",Fa,80)};function Pc(a){var b=this.plugins_;if(b)return b.get(a)} +var T=function(a,b,c,d){a[b]=function(){try{return void 0!=d&&H(d),c[ya](this,arguments)}catch(a){throw Ra("exc",b,a&&a[r]),a;}}},Qc=function(a,b,c,d){U[x][a]=function(){try{return H(c),Aa(this.a.get(b),d)}catch(e){throw Ra("exc",a,e&&e[r]),e;}}},V=function(a,b,c,d,e){U[x][a]=function(f){try{H(c),void 0==e?this.a.set(b,Aa(f,d)):this.a.set(b,e)}catch(Be){throw Ra("exc",a,Be&&Be[r]),Be;}}},Te=function(a,b){return{type:b,target:a,stopPropagation:function(){throw"aborted";}}};var Rc=new RegExp(/(^|\.)doubleclick\.net$/i),Sc=function(a,b){return Rc[ia](J[z].hostname)?!0:"/"!==b?!1:0!=a[q]("www.google.")&&0!=a[q](".google.")&&0!=a[q]("google.")||-1b[w]||ad(b[0],c))return!1;b=b[ja](1)[C](".")[y]("|");0=b[w])return!0; +b=b[1][y](-1==b[1][q](",")?"^":",");for(c=0;cb[w]||ad(b[0],c))return a.set(ec,void 0),a.set(fc,void 0),a.set(gc,void 0),a.set(ic,void 0),a.set(jc,void 0),a.set(nc,void 0),a.set(oc,void 0),a.set(pc,void 0),a.set(qc,void 0),a.set(S,void 0),a.set(kc,void 0),a.set(lc,void 0),a.set(mc,void 0),!1;a.set(ec,1*b[1]);a.set(fc,1*b[2]);a.set(gc,1*b[3]);Ve(a,b[ja](4)[C]("."));return!0},Ve=function(a,b){function c(a){return(a=b[oa](a+"=(.*?)(?:\\|utm|$)"))&& +2==a[w]?a[1]:void 0}function d(b,c){c?(c=e?I(c):c[y]("%20")[C](" "),a.set(b,c)):a.set(b,void 0)}-1==b[q]("=")&&(b=I(b));var e="2"==c("utmcvr");d(ic,c("utmcid"));d(jc,c("utmccn"));d(nc,c("utmcsr"));d(oc,c("utmcmd"));d(pc,c("utmctr"));d(qc,c("utmcct"));d(S,c("utmgclid"));d(kc,c("utmgclsrc"));d(lc,c("utmdclid"));d(mc,c("utmdsid"))},ad=function(a,b){return b?a!=b:!/^\d+$/[ia](a)};var Uc=function(){this.filters=[]};Uc[x].add=function(a,b){this.filters[n]({name:a,s:b})};Uc[x].cb=function(a){try{for(var b=0;b=100*a.get(vb)&&a[ta]()}function kd(a){ld(a.get(Wa))&&a[ta]()}function md(a){"file:"==J[z][A]&&a[ta]()}function Ge(a){He()&&a[ta]()}function nd(a){a.get(Ib)||a.set(Ib,J.title,!0);a.get(Hb)||a.set(Hb,J[z].pathname+J[z][va],!0)} +function lf(a){a.get(Wa)&&"UA-XXXXX-X"!=a.get(Wa)||a[ta]()};var od=new function(){var a=[];this.set=function(b){a[b]=!0};this.encode=function(){for(var b=[],c=0;c=b[0]||0>=b[1]?"":b[C]("x");a.Wa=d}catch(k){H(135)}qd=a}},td=function(){sd();for(var a= +qd,b=W[za],a=b.appName+b.version+a.language+b.platform+b.userAgent+a.javaEnabled+a.Q+a.P+(J.cookie?J.cookie:"")+(J.referrer?J.referrer:""),b=a[w],c=W.history[w];0d?(this.i=b[B](0,d),this.l=b[B](d+1,c),this.h=b[B](c+1)):(this.i=b[B](0,d),this.h=b[B](d+1));this.k=a[ja](1);this.Ma=!this.l&&"_require"==this.h;this.J=!this.i&&!this.l&&"_provide"==this.h}},Y=function(){T(Y[x],"push",Y[x][n],5);T(Y[x],"_getPlugin",Pc,121);T(Y[x], +"_createAsyncTracker",Y[x].Sa,33);T(Y[x],"_getAsyncTracker",Y[x].Ta,34);this.I=new nf;this.p=[]};E=Y[x];E.Na=function(a,b,c){var d=this.I.get(a);if(!Ba(d))return!1;b.plugins_=b.plugins_||new nf;b.plugins_.set(a,new d(b,c||{}));return!0};E.push=function(a){var b=Z.Va[ya](this,arguments),b=Z.p.concat(b);for(Z.p=[];0e?b+"#"+d:b+"&"+d;c="";f=b[q]("?");0f?b+"?"+d+c:b+"&"+d+c},$d=function(a,b,c,d){for(var e=0;3>e;e++){for(var f=0;3>f;f++){if(d==Yc(a+b+c))return H(127),[b,c]; +var Be=b[p](/ /g,"%20"),k=c[p](/ /g,"%20");if(d==Yc(a+Be+k))return H(128),[Be,k];Be=Be[p](/\+/g,"%20");k=k[p](/\+/g,"%20");if(d==Yc(a+Be+k))return H(129),[Be,k];try{var Ja=b[oa]("utmctr=(.*?)(?:\\|utm|$)");if(Ja&&2==Ja[w]&&(Be=b[p](Ja[1],G(I(Ja[1]))),d==Yc(a+Be+c)))return H(139),[Be,c]}catch(t){}b=I(b)}c=I(c)}};var de="|",fe=function(a,b,c,d,e,f,Be,k,Ja){var t=ee(a,b);t||(t={},a.get(Cb)[n](t));t.id_=b;t.affiliation_=c;t.total_=d;t.tax_=e;t.shipping_=f;t.city_=Be;t.state_=k;t.country_=Ja;t.items_=t.items_||[];return t},ge=function(a,b,c,d,e,f,Be){a=ee(a,b)||fe(a,b,"",0,0,0,"","","");var k;t:{if(a&&a.items_){k=a.items_;for(var Ja=0;Jab[w]||!/^\d+$/[ia](b[0])||(b[0]=""+c,Fd(a,"__utmx",b[C]("."),void 0))},be=function(a,b){var c=$c(a.get(O),pd("__utmx"));"-"==c&&(c="");return b?G(c):c},Ye=function(a){try{var b=La(J[z][xa],!1),c=ea(L(b.d.get("utm_referrer")))||"";c&&a.set(Jb,c);var d=ea(K(b.d.get("utm_expid")))||"";d&&(d=d[y](".")[0],a.set(Oc,""+d))}catch(e){H(146)}},l=function(a){var b=W.gaData&&W.gaData.expId;b&&a.set(Oc,""+b)};var ke=function(a,b){var c=m.min(a.b(Dc,0),100);if(a.b(Q,0)%100>=c)return!1;c=Ze()||$e();if(void 0==c)return!1;var d=c[0];if(void 0==d||d==ba||da(d))return!1;0a[b])return!1;return!0},le=function(a){return da(a)|| +0>a?0:5E3>a?10*m[la](a/10):5E4>a?100*m[la](a/100):41E5>a?1E3*m[la](a/1E3):41E5},je=function(a){for(var b=new yd,c=0;cc[w])){for(var d=[],e=0;e=f)return!1;c=1*(""+c);if(""==a||!wd(a)||""==b||!wd(b)||!xd(c)||da(c)||0>c||0>f||100=a||a>e.get(yb))a=!1;else if(!b||!c||128=a&&Ca(b)&&""!=b){var c=this.get(Fc)||[];c[a]=b;this.set(Fc,c)}};E.V=function(a){a=""+a;if(a[oa](/^[A-Za-z0-9]{1,5}$/)){var b=this.get(Ic)||[];b[n](a);this.set(Ic,b)}};E.v=function(){this.a[ka]()};E.Ba=function(a){a&&""!=a&&(this.set(Tb,a),this.a.j("var"))};var ne=function(a){"trans"!==a.get(sc)&&500<=a.b(cc,0)&&a[ta]();if("event"===a.get(sc)){var b=(new Date)[g](),c=a.b(dc,0),d=a.b(Zb,0),c=m[la]((b-(c!=d?c:1E3*c))/1E3*1);0=a.b(R,0)&&a[ta]()}},pe=function(a){"event"===a.get(sc)&&a.set(R,m.max(0,a.b(R,10)-1))};var qe=function(){var a=[];this.add=function(b,c,d){d&&(c=G(""+c));a[n](b+"="+c)};this.toString=function(){return a[C]("&")}},re=function(a,b){(b||2!=a.get(xb))&&a.Za(cc)},se=function(a,b){b.add("utmwv","5.6.3");b.add("utms",a.get(cc));b.add("utmn",Ea());var c=J[z].hostname;F(c)||b.add("utmhn",c,!0);c=a.get(vb);100!=c&&b.add("utmsp",c,!0)},te=function(a,b){b.add("utmht",(new Date)[g]());b.add("utmac",Da(a.get(Wa)));a.get(Oc)&&b.add("utmxkey",a.get(Oc),!0);a.get(vc)&&b.add("utmni",1);a.get(of)&&b.add("utmgtm", +a.get(of),!0);var c=a.get(Ic);c&&0=a[w])gf(a,b,c);else if(8192>=a[w]){if(0<=W[za].userAgent[q]("Firefox")&&![].reduce)throw new De(a[w]);df(a,b)||ef(a,b)||Ee(a,b)||b()}else throw new Ce(a[w]);},gf=function(a,b,c){c=c||Ne()+"/__utm.gif?";var d=new Image(1,1);d.src=c+ +a;Ie(d,function(){Ie(d,null);Je(d,null);b()});Je(d,function(){Ie(d,null);Je(d,null);b()})},ef=function(a,b){var c;c=W.XDomainRequest;if(!c)return!1;c=new c;c.open("POST",Ne()+"/p/__utm.gif");Je(c,function(){b()});Ie(c,b);c.send(a);return!0},df=function(a,b){var c=W.XMLHttpRequest;if(!c)return!1;var d=new c;if(!("withCredentials"in d))return!1;d.open("POST",Ne()+"/p/__utm.gif",!0);d.withCredentials=!0;d.setRequestHeader("Content-Type","text/plain");d.onreadystatechange=function(){4==d.readyState&& +(b(),d=null)};d.send(a);return!0},Ee=function(a,b){if(!J.body)return We(function(){Ee(a,b)},100),!0;a=aa(a);try{var c=J[qa]('')}catch(d){c=J[qa]("iframe"),ha(c,a)}c.height="0";c.width="0";c.style.display="none";c.style.visibility="hidden";var e=Ne()+"/u/post_iframe.html";Ga(W,"beforeunload",function(){c.src="";c.parentNode&&c.parentNode.removeChild(c)});ca(b,1E3);J.body.appendChild(c);c.src=e;return!0};var qf=function(){this.G=this.w=!1;0==Ea()%1E4&&(H(142),this.G=!0);this.C={};this.D=[];this.U=0;this.S=[["www.google-analytics.com","","/plugins/"]];this._gasoCPath=this._gasoDomain=this.bb=void 0;Re();Se()};E=qf[x];E.oa=function(a,b){return this.r(a,void 0,b)};E.r=function(a,b,c){b&&H(23);c&&H(67);void 0==b&&(b="~"+M.U++);a=new U(b,a,c);M.C[b]=a;M.D[n](a);return a};E.u=function(a){a=a||"";return M.C[a]||M.r(void 0,a)};E.pa=function(){return M.D[ja](0)};E.ab=function(){return M.D[w]}; +E.aa=function(){this.w=!0};E.la=function(){this.G=!0};var Fe=function(a){if("prerender"==J.visibilityState)return!1;a();return!0};var M=new qf;var Ha=W._gat;Ha&&Ba(Ha._getTracker)?M=Ha:W._gat=M;var Z=new Y;(function(a){if(!Fe(a)){H(123);var b=!1,c=function(){if(!b&&Fe(a)){b=!0;var d=J,e=c;d.removeEventListener?d.removeEventListener("visibilitychange",e,!1):d.detachEvent&&d.detachEvent("onvisibilitychange",e)}};Ga(J,"visibilitychange",c)}})(function(){var a=W._gaq,b=!1;if(a&&Ba(a[n])&&(b="[object Array]"==Object[x][v].call(Object(a)),!b)){Z=a;return}W._gaq=Z;b&&Z[n][ya](Z,a)});function Yc(a){var b=1,c=0,d;if(a)for(b=0,d=a[w]-1;0<=d;d--)c=a.charCodeAt(d),b=(b<<6&268435455)+c+(c<<14),c=b&266338304,b=0!=c?b^c>>21:b;return b};})(); diff --git a/sandbox/ProgressButtonStyles/js/modernizr.custom.js b/sandbox/ProgressButtonStyles/js/modernizr.custom.js new file mode 100644 index 0000000..8c3d405 --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/modernizr.custom.js @@ -0,0 +1,4 @@ +/* Modernizr 2.7.1 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-csstransforms3d-csstransitions-shiv-cssclasses-prefixed-teststyles-testprop-testallprops-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function z(a){j.cssText=a}function A(a,b){return z(m.join(a+";")+(b||""))}function B(a,b){return typeof a===b}function C(a,b){return!!~(""+a).indexOf(b)}function D(a,b){for(var d in a){var e=a[d];if(!C(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function E(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:B(f,"function")?f.bind(d||b):f}return!1}function F(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return B(b,"string")||B(b,"undefined")?D(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),E(e,b,c))}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={},r={},s={},t=[],u=t.slice,v,w=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},x={}.hasOwnProperty,y;!B(x,"undefined")&&!B(x.call,"undefined")?y=function(a,b){return x.call(a,b)}:y=function(a,b){return b in a&&B(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=u.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(u.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(u.call(arguments)))};return e}),q.csstransforms3d=function(){var a=!!F("perspective");return a&&"webkitPerspective"in g.style&&w("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},q.csstransitions=function(){return F("transition")};for(var G in q)y(q,G)&&(v=G.toLowerCase(),e[v]=q[G](),t.push((e[v]?"":"no-")+v));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)y(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},z(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.testProp=function(a){return D([a])},e.testAllProps=F,e.testStyles=w,e.prefixed=function(a,b,c){return b?F(a,b,c):F(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+t.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f= 0 ? 'state-success' : 'state-error'; + classie.add( self.button, statusClass ); + // after options.statusTime remove status + setTimeout( function() { + classie.remove( self.button, statusClass ); + self._enable(); + }, self.options.statusTime ); + } + else { + self._enable(); + } + + // remove class state-loading from the button + classie.remove( self.button, 'state-loading' ); + }, 100 ); + }; + + // enable button + ProgressButton.prototype._enable = function() { + this.button.removeAttribute( 'disabled' ); + } + + // add to global namespace + window.ProgressButton = ProgressButton; + +})( window ); \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/saved_resource b/sandbox/ProgressButtonStyles/js/saved_resource new file mode 100644 index 0000000..88e703c --- /dev/null +++ b/sandbox/ProgressButtonStyles/js/saved_resource @@ -0,0 +1 @@ +_carbonads_go({"ads":[{"image":"https://launchbit.com/taz-i/5933-17f65e6063df008f3c413fea2ab9659188658b08-bsa.jpg","statlink":"http://www.launchbit.com/taz/9533-5933-111/","description":"PR6 Links From $15 Get Quality One Way Text Backlinks with PR6 From $15 per Link","pixel":"https://www.launchbit.com/taz-pixel/9533-5933-111/"}]}); \ No newline at end of file diff --git a/sandbox/ProgressButtonStyles/js/saved_resource(1) b/sandbox/ProgressButtonStyles/js/saved_resource(1) new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/sandbox/ProgressButtonStyles/js/saved_resource(1) differ diff --git a/sandbox/audioPlusVisualizer/Audio Recorder.html b/sandbox/audioPlusVisualizer/Audio Recorder.html new file mode 100644 index 0000000..d29c26c --- /dev/null +++ b/sandbox/audioPlusVisualizer/Audio Recorder.html @@ -0,0 +1,72 @@ + + + + + Audio Recorder + + + + + + + +
+ + +
+
+ + +
+ + diff --git a/sandbox/audioPlusVisualizer/js/audiodisplay.js b/sandbox/audioPlusVisualizer/js/audiodisplay.js new file mode 100644 index 0000000..043da42 --- /dev/null +++ b/sandbox/audioPlusVisualizer/js/audiodisplay.js @@ -0,0 +1,18 @@ +function drawBuffer( width, height, context, data ) { + var step = Math.ceil( data.length / width ); + var amp = height / 2; + context.fillStyle = "silver"; + context.clearRect(0,0,width,height); + for(var i=0; i < width; i++){ + var min = 1.0; + var max = -1.0; + for (j=0; j max) + max = datum; + } + context.fillRect(i,(1+min)*amp,1,Math.max(1,(max-min)*amp)); + } +} diff --git a/sandbox/audioPlusVisualizer/js/main.js b/sandbox/audioPlusVisualizer/js/main.js new file mode 100644 index 0000000..29689d6 --- /dev/null +++ b/sandbox/audioPlusVisualizer/js/main.js @@ -0,0 +1,186 @@ +/* Copyright 2013 Chris Wilson + + 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. +*/ + +window.AudioContext = window.AudioContext || window.webkitAudioContext; + +var audioContext = new AudioContext(); +var audioInput = null, + realAudioInput = null, + inputPoint = null, + audioRecorder = null; +var rafID = null; +var analyserContext = null; +var canvasWidth, canvasHeight; +var recIndex = 0; + +/* TODO: + +- offer mono option +- "Monitor input" switch +*/ + +function saveAudio() { + audioRecorder.exportWAV( doneEncoding ); + // could get mono instead by saying + // audioRecorder.exportMonoWAV( doneEncoding ); +} + +function gotBuffers( buffers ) { + var canvas = document.getElementById( "wavedisplay" ); + + drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] ); + + // the ONLY time gotBuffers is called is right after a new recording is completed - + // so here's where we should set up the download. + audioRecorder.exportWAV( doneEncoding ); +} + +function doneEncoding( blob ) { + Recorder.setupDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" ); + recIndex++; +} + +function toggleRecording( e ) { + if (e.classList.contains("recording")) { + // stop recording + audioRecorder.stop(); + e.classList.remove("recording"); + audioRecorder.getBuffers( gotBuffers ); + } else { + // start recording + if (!audioRecorder) + return; + e.classList.add("recording"); + audioRecorder.clear(); + audioRecorder.record(); + } +} + +function convertToMono( input ) { + var splitter = audioContext.createChannelSplitter(2); + var merger = audioContext.createChannelMerger(2); + + input.connect( splitter ); + splitter.connect( merger, 0, 0 ); + splitter.connect( merger, 0, 1 ); + return merger; +} + +function cancelAnalyserUpdates() { + window.cancelAnimationFrame( rafID ); + rafID = null; +} + +function updateAnalysers(time) { + if (!analyserContext) { + var canvas = document.getElementById("analyser"); + canvasWidth = canvas.width; + canvasHeight = canvas.height; + analyserContext = canvas.getContext('2d'); + } + + // analyzer draw code here + { + var SPACING = 3; + var BAR_WIDTH = 1; + var numBars = Math.round(canvasWidth / SPACING); + var freqByteData = new Uint8Array(analyserNode.frequencyBinCount); + + analyserNode.getByteFrequencyData(freqByteData); + + analyserContext.clearRect(0, 0, canvasWidth, canvasHeight); + analyserContext.fillStyle = '#F6D565'; + analyserContext.lineCap = 'round'; + var multiplier = analyserNode.frequencyBinCount / numBars; + + // Draw rectangle for each frequency bin. + for (var i = 0; i < numBars; ++i) { + var magnitude = 0; + var offset = Math.floor( i * multiplier ); + // gotta sum/average the block, or we miss narrow-bandwidth spikes + for (var j = 0; j< multiplier; j++) + magnitude += freqByteData[offset + j]; + magnitude = magnitude / multiplier; + var magnitude2 = freqByteData[i * multiplier]; + analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)"; + analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude); + } + } + + rafID = window.requestAnimationFrame( updateAnalysers ); +} + +function toggleMono() { + if (audioInput != realAudioInput) { + audioInput.disconnect(); + realAudioInput.disconnect(); + audioInput = realAudioInput; + } else { + realAudioInput.disconnect(); + audioInput = convertToMono( realAudioInput ); + } + + audioInput.connect(inputPoint); +} + +function gotStream(stream) { + inputPoint = audioContext.createGain(); + + // Create an AudioNode from the stream. + realAudioInput = audioContext.createMediaStreamSource(stream); + audioInput = realAudioInput; + audioInput.connect(inputPoint); + +// audioInput = convertToMono( input ); + + analyserNode = audioContext.createAnalyser(); + analyserNode.fftSize = 2048; + inputPoint.connect( analyserNode ); + + audioRecorder = new Recorder( inputPoint ); + + zeroGain = audioContext.createGain(); + zeroGain.gain.value = 0.0; + inputPoint.connect( zeroGain ); + zeroGain.connect( audioContext.destination ); + updateAnalysers(); +} + +function initAudio() { + if (!navigator.getUserMedia) + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + if (!navigator.cancelAnimationFrame) + navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame; + if (!navigator.requestAnimationFrame) + navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame; + + navigator.getUserMedia( + { + "audio": { + "mandatory": { + "googEchoCancellation": "false", + "googAutoGainControl": "false", + "googNoiseSuppression": "false", + "googHighpassFilter": "false" + }, + "optional": [] + }, + }, gotStream, function(e) { + alert('Error getting audio'); + console.log(e); + }); +} + +window.addEventListener('load', initAudio ); diff --git a/sandbox/audioPlusVisualizer/js/mic128.png b/sandbox/audioPlusVisualizer/js/mic128.png new file mode 100644 index 0000000..b9ed1a0 Binary files /dev/null and b/sandbox/audioPlusVisualizer/js/mic128.png differ diff --git a/sandbox/audioPlusVisualizer/js/recorder.js b/sandbox/audioPlusVisualizer/js/recorder.js new file mode 100644 index 0000000..b469ba4 --- /dev/null +++ b/sandbox/audioPlusVisualizer/js/recorder.js @@ -0,0 +1,118 @@ +/*License (MIT) + +Copyright © 2013 Matt Diamond + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +(function(window){ + + var WORKER_PATH = 'js/recorderjs/recorderWorker.js'; + + var Recorder = function(source, cfg){ + var config = cfg || {}; + var bufferLen = config.bufferLen || 4096; + this.context = source.context; + if(!this.context.createScriptProcessor){ + this.node = this.context.createJavaScriptNode(bufferLen, 2, 2); + } else { + this.node = this.context.createScriptProcessor(bufferLen, 2, 2); + } + + var worker = new Worker(config.workerPath || WORKER_PATH); + worker.postMessage({ + command: 'init', + config: { + sampleRate: this.context.sampleRate + } + }); + var recording = false, + currCallback; + + this.node.onaudioprocess = function(e){ + if (!recording) return; + worker.postMessage({ + command: 'record', + buffer: [ + e.inputBuffer.getChannelData(0), + e.inputBuffer.getChannelData(1) + ] + }); + } + + this.configure = function(cfg){ + for (var prop in cfg){ + if (cfg.hasOwnProperty(prop)){ + config[prop] = cfg[prop]; + } + } + } + + this.record = function(){ + recording = true; + } + + this.stop = function(){ + recording = false; + } + + this.clear = function(){ + worker.postMessage({ command: 'clear' }); + } + + this.getBuffers = function(cb) { + currCallback = cb || config.callback; + worker.postMessage({ command: 'getBuffers' }) + } + + this.exportWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportWAV', + type: type + }); + } + + this.exportMonoWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportMonoWAV', + type: type + }); + } + + worker.onmessage = function(e){ + var blob = e.data; + currCallback(blob); + } + + source.connect(this.node); + this.node.connect(this.context.destination); // if the script node is not connected to an output the "onaudioprocess" event is not triggered in chrome. + }; + + Recorder.setupDownload = function(blob, filename){ + var url = (window.URL || window.webkitURL).createObjectURL(blob); + var link = document.getElementById("save"); + link.href = url; + link.download = filename || 'output.wav'; + } + + window.Recorder = Recorder; + +})(window); diff --git a/sandbox/audioPlusVisualizer/js/recorderWorker.js b/sandbox/audioPlusVisualizer/js/recorderWorker.js new file mode 100644 index 0000000..dce4442 --- /dev/null +++ b/sandbox/audioPlusVisualizer/js/recorderWorker.js @@ -0,0 +1,161 @@ +/*License (MIT) + + Copyright © 2013 Matt Diamond + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of + the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + */ + +var recLength = 0, + recBuffersL = [], + recBuffersR = [], + sampleRate; + +this.onmessage = function(e){ + switch(e.data.command){ + case 'init': + init(e.data.config); + break; + case 'record': + record(e.data.buffer); + break; + case 'exportWAV': + exportWAV(e.data.type); + break; + case 'exportMonoWAV': + exportMonoWAV(e.data.type); + break; + case 'getBuffers': + getBuffers(); + break; + case 'clear': + clear(); + break; + } +}; + +function init(config){ + sampleRate = config.sampleRate; +} + +function record(inputBuffer){ + recBuffersL.push(inputBuffer[0]); + recBuffersR.push(inputBuffer[1]); + recLength += inputBuffer[0].length; +} + +function exportWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var bufferR = mergeBuffers(recBuffersR, recLength); + var interleaved = interleave(bufferL, bufferR); + var dataview = encodeWAV(interleaved); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function exportMonoWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var dataview = encodeWAV(bufferL, true); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function getBuffers() { + var buffers = []; + buffers.push( mergeBuffers(recBuffersL, recLength) ); + buffers.push( mergeBuffers(recBuffersR, recLength) ); + this.postMessage(buffers); +} + +function clear(){ + recLength = 0; + recBuffersL = []; + recBuffersR = []; +} + +function mergeBuffers(recBuffers, recLength){ + var result = new Float32Array(recLength); + var offset = 0; + for (var i = 0; i < recBuffers.length; i++){ + result.set(recBuffers[i], offset); + offset += recBuffers[i].length; + } + return result; +} + +function interleave(inputL, inputR){ + var length = inputL.length + inputR.length; + var result = new Float32Array(length); + + var index = 0, + inputIndex = 0; + + while (index < length){ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; + } + return result; +} + +function floatTo16BitPCM(output, offset, input){ + for (var i = 0; i < input.length; i++, offset+=2){ + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeString(view, offset, string){ + for (var i = 0; i < string.length; i++){ + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function encodeWAV(samples, mono){ + var buffer = new ArrayBuffer(44 + samples.length * 2); + var view = new DataView(buffer); + + /* RIFF identifier */ + writeString(view, 0, 'RIFF'); + /* file length */ + view.setUint32(4, 32 + samples.length * 2, true); + /* RIFF type */ + writeString(view, 8, 'WAVE'); + /* format chunk identifier */ + writeString(view, 12, 'fmt '); + /* format chunk length */ + view.setUint32(16, 16, true); + /* sample format (raw) */ + view.setUint16(20, 1, true); + /* channel count */ + view.setUint16(22, mono?1:2, true); + /* sample rate */ + view.setUint32(24, sampleRate, true); + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * 4, true); + /* block align (channel count * bytes per sample) */ + view.setUint16(32, 4, true); + /* bits per sample */ + view.setUint16(34, 16, true); + /* data chunk identifier */ + writeString(view, 36, 'data'); + /* data chunk length */ + view.setUint32(40, samples.length * 2, true); + + floatTo16BitPCM(view, 44, samples); + + return view; +} diff --git a/sandbox/audioPlusVisualizer/js/save.svg b/sandbox/audioPlusVisualizer/js/save.svg new file mode 100644 index 0000000..5838f7a --- /dev/null +++ b/sandbox/audioPlusVisualizer/js/save.svg @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Save + + + Jakub Steiner + + + + + hdd + hard drive + save + io + store + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandbox/audioTest/Audio Recorder.html b/sandbox/audioTest/Audio Recorder.html new file mode 100644 index 0000000..0c7a68f --- /dev/null +++ b/sandbox/audioTest/Audio Recorder.html @@ -0,0 +1,72 @@ + + + + + Audio Recorder + + + + + + + +
+ + +
+
+ + +
+ + \ No newline at end of file diff --git a/sandbox/audioTest/js/audiodisplay.js b/sandbox/audioTest/js/audiodisplay.js new file mode 100644 index 0000000..a99622c --- /dev/null +++ b/sandbox/audioTest/js/audiodisplay.js @@ -0,0 +1,18 @@ +function drawBuffer( width, height, context, data ) { + var step = Math.ceil( data.length / width ); + var amp = height / 2; + context.fillStyle = "silver"; + context.clearRect(0,0,width,height); + for(var i=0; i < width; i++){ + var min = 1.0; + var max = -1.0; + for (j=0; j max) + max = datum; + } + context.fillRect(i,(1+min)*amp,1,Math.max(1,(max-min)*amp)); + } +} diff --git a/sandbox/audioTest/js/main.js b/sandbox/audioTest/js/main.js new file mode 100644 index 0000000..e0471ae --- /dev/null +++ b/sandbox/audioTest/js/main.js @@ -0,0 +1,200 @@ +/* Copyright 2013 Chris Wilson + + 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. +*/ + +window.AudioContext = window.AudioContext || window.webkitAudioContext; + +var audioContext = new AudioContext(); +var audioInput = null, + realAudioInput = null, + inputPoint = null, + audioRecorder = null; +var rafID = null; +var analyserContext = null; +var canvasWidth, canvasHeight; +var recIndex = 0; + +/* TODO: + +- offer mono option +- "Monitor input" switch +*/ + +function saveAudio() { + audioRecorder.exportWAV( doneEncoding ); + // could get mono instead by saying + // audioRecorder.exportMonoWAV( doneEncoding ); +} + +function gotBuffers( buffers ) { + var canvas = document.getElementById( "wavedisplay" ); + console.log('gotBuffers'); + debugger; + drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] ); + + // the ONLY time gotBuffers is called is right after a new recording is completed - + // so here's where we should set up the download. + audioRecorder.exportWAV( doneEncoding ); +} + +function doneEncoding( blob ) { + Recorder.setupDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" ); + recIndex++; +} + +function toggleRecording( e ) { + if (e.classList.contains("recording")) { + // stop recording + audioRecorder.stop(); + e.classList.remove("recording"); + audioRecorder.getBuffers( gotBuffers ); + } else { + // start recording + if (!audioRecorder) + return; + e.classList.add("recording"); + audioRecorder.clear(); + audioRecorder.record(); + } +} + +function convertToMono( input ) { + var splitter = audioContext.createChannelSplitter(2); + var merger = audioContext.createChannelMerger(2); + + input.connect( splitter ); + splitter.connect( merger, 0, 0 ); + splitter.connect( merger, 0, 1 ); + return merger; +} + +function cancelAnalyserUpdates() { + window.cancelAnimationFrame( rafID ); + rafID = null; +} + +function updateAnalysers(time) { + if (!analyserContext) { + var canvas = document.getElementById("canvas"); + canvasWidth = canvas.width; + canvasHeight = canvas.height; + analyserContext = canvas.getContext('2d'); + } + + // analyzer draw code here + { + var SPACING = 3; + var BAR_WIDTH = 1; + var numBars = Math.round(canvasWidth / SPACING); + var freqByteData = new Uint8Array(analyserNode.frequencyBinCount); + + analyserNode.getByteFrequencyData(freqByteData); + + analyserContext.clearRect(0, 0, canvasWidth, canvasHeight); + analyserContext.fillStyle = '#F6D565'; + analyserContext.lineCap = 'round'; + var multiplier = analyserNode.frequencyBinCount / numBars; + + // Draw rectangle for each frequency bin. + for (var i = 0; i < numBars; ++i) { + var magnitude = 0; + var offset = Math.floor( i * multiplier ); + // gotta sum/average the block, or we miss narrow-bandwidth spikes + for (var j = 0; j< multiplier; j++) + magnitude += freqByteData[offset + j]; + magnitude = magnitude / multiplier; + var magnitude2 = freqByteData[i * multiplier]; + analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)"; + analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude); + } + } + + rafID = window.requestAnimationFrame( updateAnalysers ); +} + +function toggleMono() { + if (audioInput != realAudioInput) { + audioInput.disconnect(); + realAudioInput.disconnect(); + audioInput = realAudioInput; + } else { + realAudioInput.disconnect(); + audioInput = convertToMono( realAudioInput ); + } + + audioInput.connect(inputPoint); +} + +function gotStream(stream) { + // Once we have a successful stream we can initialize the rest of our audio shit + // Audio Context, {} + // MediaStreamAudioSourceNode {} (visual?) + // Gain Node, {} (volume of audio graph) + + console.log(audioContext); + + // Takes created AudioContext and creates a gainNode + inputPoint = audioContext.createGain(); + console.log(inputPoint); + + // Create a MediaStreamSource from our stream object AudioNode from the stream. + realAudioInput = audioContext.createMediaStreamSource(stream); + console.log(realAudioInput); + audioInput = realAudioInput; + audioInput.connect(inputPoint); + +// audioInput = convertToMono( input ); + + // Visual + analyserNode = audioContext.createAnalyser(); + analyserNode.fftSize = 2048; + inputPoint.connect( analyserNode ); + + // Creates Recorder object + audioRecorder = new Recorder( inputPoint ); + + + zeroGain = audioContext.createGain(); + zeroGain.gain.value = 0.0; + inputPoint.connect( zeroGain ); + zeroGain.connect( audioContext.destination ); + updateAnalysers(); +} + +function initAudio() { + if (!navigator.getUserMedia) + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + if (!navigator.cancelAnimationFrame) + navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame; + if (!navigator.requestAnimationFrame) + navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame; + + navigator.getUserMedia( + { + "audio": { + "mandatory": { + "googEchoCancellation": "false", + "googAutoGainControl": "false", + "googNoiseSuppression": "false", + "googHighpassFilter": "false" + }, + "optional": [] + }, + }, gotStream, function(e) { + alert('Error getting audio'); + console.log(e); + }); +} + +window.addEventListener('load', initAudio ); diff --git a/sandbox/audioTest/js/mic128.png b/sandbox/audioTest/js/mic128.png new file mode 100644 index 0000000..b9ed1a0 Binary files /dev/null and b/sandbox/audioTest/js/mic128.png differ diff --git a/sandbox/audioTest/js/recorder.js b/sandbox/audioTest/js/recorder.js new file mode 100644 index 0000000..d3941dc --- /dev/null +++ b/sandbox/audioTest/js/recorder.js @@ -0,0 +1,121 @@ +/*License (MIT) + +Copyright © 2013 Matt Diamond + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +(function(window){ + + var WORKER_PATH = 'js/recorderWorker.js'; + + var Recorder = function(source, cfg){ + var config = cfg || {}; + console.log('config ->'); + console.log(config); + var bufferLen = config.bufferLen || 4096; + this.context = source.context; + if(!this.context.createScriptProcessor){ + this.node = this.context.createJavaScriptNode(bufferLen, 2, 2); + } else { + this.node = this.context.createScriptProcessor(bufferLen, 2, 2); + } + + var worker = new Worker(config.workerPath || WORKER_PATH); + worker.postMessage({ + command: 'init', + config: { + sampleRate: this.context.sampleRate + } + }); + var recording = false, + currCallback; + + this.node.onaudioprocess = function(e){ + if (!recording) return; + worker.postMessage({ + command: 'record', + buffer: [ + e.inputBuffer.getChannelData(0), + e.inputBuffer.getChannelData(1) + ] + }); + } + + this.configure = function(cfg){ + for (var prop in cfg){ + if (cfg.hasOwnProperty(prop)){ + config[prop] = cfg[prop]; + } + } + } + + this.record = function(){ + recording = true; + } + + this.stop = function(){ + recording = false; + } + + this.clear = function(){ + worker.postMessage({ command: 'clear' }); + } + + this.getBuffers = function(cb) { + currCallback = cb || config.callback; + worker.postMessage({ command: 'getBuffers' }) + } + + this.exportWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportWAV', + type: type + }); + } + + this.exportMonoWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportMonoWAV', + type: type + }); + } + + worker.onmessage = function(e){ + console.log(e); + var blob = e.data; + currCallback(blob); + } + + source.connect(this.node); + this.node.connect(this.context.destination); // if the script node is not connected to an output the "onaudioprocess" event is not triggered in chrome. + }; + + Recorder.setupDownload = function(blob, filename){ + var url = (window.URL || window.webkitURL).createObjectURL(blob); + var link = document.getElementById("save"); + link.href = url; + link.download = filename || 'output.wav'; + } + + window.Recorder = Recorder; + +})(window); diff --git a/sandbox/audioTest/js/recorderWorker.js b/sandbox/audioTest/js/recorderWorker.js new file mode 100644 index 0000000..dce4442 --- /dev/null +++ b/sandbox/audioTest/js/recorderWorker.js @@ -0,0 +1,161 @@ +/*License (MIT) + + Copyright © 2013 Matt Diamond + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of + the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + */ + +var recLength = 0, + recBuffersL = [], + recBuffersR = [], + sampleRate; + +this.onmessage = function(e){ + switch(e.data.command){ + case 'init': + init(e.data.config); + break; + case 'record': + record(e.data.buffer); + break; + case 'exportWAV': + exportWAV(e.data.type); + break; + case 'exportMonoWAV': + exportMonoWAV(e.data.type); + break; + case 'getBuffers': + getBuffers(); + break; + case 'clear': + clear(); + break; + } +}; + +function init(config){ + sampleRate = config.sampleRate; +} + +function record(inputBuffer){ + recBuffersL.push(inputBuffer[0]); + recBuffersR.push(inputBuffer[1]); + recLength += inputBuffer[0].length; +} + +function exportWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var bufferR = mergeBuffers(recBuffersR, recLength); + var interleaved = interleave(bufferL, bufferR); + var dataview = encodeWAV(interleaved); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function exportMonoWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var dataview = encodeWAV(bufferL, true); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function getBuffers() { + var buffers = []; + buffers.push( mergeBuffers(recBuffersL, recLength) ); + buffers.push( mergeBuffers(recBuffersR, recLength) ); + this.postMessage(buffers); +} + +function clear(){ + recLength = 0; + recBuffersL = []; + recBuffersR = []; +} + +function mergeBuffers(recBuffers, recLength){ + var result = new Float32Array(recLength); + var offset = 0; + for (var i = 0; i < recBuffers.length; i++){ + result.set(recBuffers[i], offset); + offset += recBuffers[i].length; + } + return result; +} + +function interleave(inputL, inputR){ + var length = inputL.length + inputR.length; + var result = new Float32Array(length); + + var index = 0, + inputIndex = 0; + + while (index < length){ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; + } + return result; +} + +function floatTo16BitPCM(output, offset, input){ + for (var i = 0; i < input.length; i++, offset+=2){ + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeString(view, offset, string){ + for (var i = 0; i < string.length; i++){ + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function encodeWAV(samples, mono){ + var buffer = new ArrayBuffer(44 + samples.length * 2); + var view = new DataView(buffer); + + /* RIFF identifier */ + writeString(view, 0, 'RIFF'); + /* file length */ + view.setUint32(4, 32 + samples.length * 2, true); + /* RIFF type */ + writeString(view, 8, 'WAVE'); + /* format chunk identifier */ + writeString(view, 12, 'fmt '); + /* format chunk length */ + view.setUint32(16, 16, true); + /* sample format (raw) */ + view.setUint16(20, 1, true); + /* channel count */ + view.setUint16(22, mono?1:2, true); + /* sample rate */ + view.setUint32(24, sampleRate, true); + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * 4, true); + /* block align (channel count * bytes per sample) */ + view.setUint16(32, 4, true); + /* bits per sample */ + view.setUint16(34, 16, true); + /* data chunk identifier */ + writeString(view, 36, 'data'); + /* data chunk length */ + view.setUint32(40, samples.length * 2, true); + + floatTo16BitPCM(view, 44, samples); + + return view; +} diff --git a/sandbox/audioTest/js/save.svg b/sandbox/audioTest/js/save.svg new file mode 100644 index 0000000..5838f7a --- /dev/null +++ b/sandbox/audioTest/js/save.svg @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Save + + + Jakub Steiner + + + + + hdd + hard drive + save + io + store + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandbox/audioVisualizer/README.md b/sandbox/audioVisualizer/README.md new file mode 100755 index 0000000..708c0b1 --- /dev/null +++ b/sandbox/audioVisualizer/README.md @@ -0,0 +1,13 @@ +HTML5 Audio Visualizer +====================== + +An audio spectrum visualizer built with HTML5 Audio API + +Demo +--- +[See it in action](http://wayou.github.io/HTML5_Audio_Visualizer/). + +Screen Capture +--- + +![alt tag](https://raw.github.com/Wayou/HTML5_Audio_Visualizer/master/sources/screencapture.png) diff --git a/sandbox/audioVisualizer/audio_visualizer_single_page_version.html b/sandbox/audioVisualizer/audio_visualizer_single_page_version.html new file mode 100755 index 0000000..79e6db2 --- /dev/null +++ b/sandbox/audioVisualizer/audio_visualizer_single_page_version.html @@ -0,0 +1,306 @@ + + + + + + HTML5 Audio API showcase | Audio visualizer + + + +
+
+
+ HTML5 Audio API showcase | An Audio Viusalizer +
+ + +
+
+ +
+
+ + + + diff --git a/sandbox/audioVisualizer/index.html b/sandbox/audioVisualizer/index.html new file mode 100755 index 0000000..8f4c461 --- /dev/null +++ b/sandbox/audioVisualizer/index.html @@ -0,0 +1,26 @@ + + + + + HTML5 Audio API showcase | Audio visualizer + + + +
+
+
+ HTML5 Audio API showcase | An Audio Viusalizer +
+ + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/sandbox/audioVisualizer/js/html5_audio_visualizer.js b/sandbox/audioVisualizer/js/html5_audio_visualizer.js new file mode 100755 index 0000000..560e791 --- /dev/null +++ b/sandbox/audioVisualizer/js/html5_audio_visualizer.js @@ -0,0 +1,239 @@ +window.onload = function() { + new Visualizer().ini(); +}; +var Visualizer = function() { + this.file = null; //the current file + this.fileName = null; //the current file name + this.audioContext = null; + this.source = null; //the audio source + this.info = document.getElementById('info').innerHTML; //this used to upgrade the UI information + this.infoUpdateId = null; //to sotore the setTimeout ID and clear the interval + this.animationId = null; + this.status = 0; //flag for sound is playing 1 or stopped 0 + this.forceStop = false; + this.allCapsReachBottom = false; +}; +Visualizer.prototype = { + ini: function() { + this._prepareAPI(); + this._addEventListner(); + }, + _prepareAPI: function() { + //fix browser vender for AudioContext and requestAnimationFrame + window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; + window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; + window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; + try { + this.audioContext = new AudioContext(); + } catch (e) { + this._updateInfo('!Your browser does not support AudioContext', false); + console.log(e); + } + }, + _addEventListner: function() { + var that = this, + audioInput = document.getElementById('uploadedFile'), + dropContainer = document.getElementsByTagName("canvas")[0]; + //listen the file upload + audioInput.onchange = function() { + + if (that.audioContext===null) {return;}; + + //the if statement fixes the file selction cancle, because the onchange will trigger even the file selection been canceled + if (audioInput.files.length !== 0) { + //only process the first file + that.file = audioInput.files[0]; + that.fileName = that.file.name; + if (that.status === 1) { + //the sound is still playing but we upload another file, so set the forceStop flag to true + that.forceStop = true; + }; + document.getElementById('fileWrapper').style.opacity = 1; + that._updateInfo('Uploading', true); + //once the file is ready,start the visualizer + that._start(); + }; + }; + //listen the drag & drop + dropContainer.addEventListener("dragenter", function() { + document.getElementById('fileWrapper').style.opacity = 1; + that._updateInfo('Drop it on the page', true); + }, false); + dropContainer.addEventListener("dragover", function(e) { + e.stopPropagation(); + e.preventDefault(); + //set the drop mode + e.dataTransfer.dropEffect = 'copy'; + }, false); + dropContainer.addEventListener("dragleave", function() { + document.getElementById('fileWrapper').style.opacity = 0.2; + that._updateInfo(that.info, false); + }, false); + dropContainer.addEventListener("drop", function(e) { + e.stopPropagation(); + e.preventDefault(); + if (that.audioContext===null) {return;}; + document.getElementById('fileWrapper').style.opacity = 1; + that._updateInfo('Uploading', true); + //get the dropped file + that.file = e.dataTransfer.files[0]; + if (that.status === 1) { + document.getElementById('fileWrapper').style.opacity = 1; + that.forceStop = true; + }; + that.fileName = that.file.name; + //once the file is ready,start the visualizer + that._start(); + }, false); + }, + _start: function() { + //read and decode the file into audio array buffer + var that = this, + file = this.file, + fr = new FileReader(); + fr.onload = function(e) { + var fileResult = e.target.result; + var audioContext = that.audioContext; + if (audioContext === null) { + return; + }; + that._updateInfo('Decoding the audio', true); + audioContext.decodeAudioData(fileResult, function(buffer) { + that._updateInfo('Decode succussfully,start the visualizer', true); + that._visualize(audioContext, buffer); + }, function(e) { + that._updateInfo('!Fail to decode the file', false); + console.log(e); + }); + }; + fr.onerror = function(e) { + that._updateInfo('!Fail to read the file', false); + console.log(e); + }; + //assign the file to the reader + this._updateInfo('Starting read the file', true); + fr.readAsArrayBuffer(file); + }, + _visualize: function(audioContext, buffer) { + var audioBufferSouceNode = audioContext.createBufferSource(), + analyser = audioContext.createAnalyser(), + that = this; + //connect the source to the analyser + audioBufferSouceNode.connect(analyser); + //connect the analyser to the destination(the speaker), or we won't hear the sound + analyser.connect(audioContext.destination); + //then assign the buffer to the buffer source node + audioBufferSouceNode.buffer = buffer; + //play the source + if (!audioBufferSouceNode.start) { + audioBufferSouceNode.start = audioBufferSouceNode.noteOn; //in old browsers use noteOn method + audioBufferSouceNode.stop = audioBufferSouceNode.noteOff; //in old browsers use noteOn method + }; + //stop the previous sound if any + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId); + } + if (this.source !== null) { + this.source.stop(0); + } + audioBufferSouceNode.start(0); + this.status = 1; + this.source = audioBufferSouceNode; + audioBufferSouceNode.onended = function() { + that._audioEnd(that); + }; + this._updateInfo('Playing ' + this.fileName, false); + this.info = 'Playing ' + this.fileName; + document.getElementById('fileWrapper').style.opacity = 0.2; + this._drawSpectrum(analyser); + }, + _drawSpectrum: function(analyser) { + var that = this, + canvas = document.getElementById('canvas'), + cwidth = canvas.width, + cheight = canvas.height - 2, + meterWidth = 100, //width of the meters in the spectrum + gap = 2, //gap between meters + capHeight = 2, + capStyle = '#fff', + meterNum = 5, //count of the meters + capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame + ctx = canvas.getContext('2d'), + gradient = ctx.createLinearGradient(0, 0, 0, 300); + gradient.addColorStop(1, '#0f0'); + gradient.addColorStop(0.5, '#ff0'); + gradient.addColorStop(0, '#f00'); + var drawMeter = function() { + var array = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteFrequencyData(array); + if (that.status === 0) { + //fix when some sounds end the value still not back to zero + for (var i = array.length - 1; i >= 0; i--) { + array[i] = 0; + }; + allCapsReachBottom = true; + for (var i = capYPositionArray.length - 1; i >= 0; i--) { + allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0); + }; + if (allCapsReachBottom) { + cancelAnimationFrame(that.animationId); //since the sound is stoped and animation finished, stop the requestAnimation to prevent potential memory leak,THIS IS VERY IMPORTANT! + return; + }; + }; + var step = Math.round(array.length / meterNum); //sample limited data from the total array + ctx.clearRect(0, 0, cwidth, cheight); + for (var i = 0; i < meterNum; i++) { + var value = array[i * step]; + if (capYPositionArray.length < Math.round(meterNum)) { + capYPositionArray.push(value); + }; + ctx.fillStyle = capStyle; + //draw the cap, with transition effect + if (value < capYPositionArray[i]) { + ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); + } else { + ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); + capYPositionArray[i] = value; + }; + ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look + ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter + } + that.animationId = requestAnimationFrame(drawMeter); + } + this.animationId = requestAnimationFrame(drawMeter); + }, + _audioEnd: function(instance) { + if (this.forceStop) { + this.forceStop = false; + this.status = 1; + return; + }; + this.status = 0; + var text = 'HTML5 Audio API showcase | An Audio Viusalizer'; + document.getElementById('fileWrapper').style.opacity = 1; + document.getElementById('info').innerHTML = text; + instance.info = text; + document.getElementById('uploadedFile').value = ''; + }, + _updateInfo: function(text, processing) { + var infoBar = document.getElementById('info'), + dots = '...', + i = 0, + that = this; + infoBar.innerHTML = text + dots.substring(0, i++); + if (this.infoUpdateId !== null) { + clearTimeout(this.infoUpdateId); + }; + if (processing) { + //animate dots at the end of the info text + var animateDot = function() { + if (i > 3) { + i = 0 + }; + infoBar.innerHTML = text + dots.substring(0, i++); + that.infoUpdateId = setTimeout(animateDot, 250); + } + this.infoUpdateId = setTimeout(animateDot, 250); + }; + } +} diff --git a/sandbox/audioVisualizer/sources/screencapture.png b/sandbox/audioVisualizer/sources/screencapture.png new file mode 100755 index 0000000..76a7843 Binary files /dev/null and b/sandbox/audioVisualizer/sources/screencapture.png differ diff --git a/sandbox/audioVisualizer/style/style.css b/sandbox/audioVisualizer/style/style.css new file mode 100755 index 0000000..a363175 --- /dev/null +++ b/sandbox/audioVisualizer/style/style.css @@ -0,0 +1,21 @@ +html, body { + margin: 0; + font-family: arial, "Microsoft YaHei"; + background-color: #272822; + color: #FEFEFE; +} +#fileWrapper{ + transition:all 0.5s ease; +} +#fileWrapper:hover{ + opacity: 1!important; +} + +#visualizer_wrapper{ + text-align: center; +} +footer{ + position: fixed; + bottom: 2px; + color:#aaa; +} \ No newline at end of file diff --git a/sandbox/index.html b/sandbox/index.html index 04b21d1..4847586 100644 --- a/sandbox/index.html +++ b/sandbox/index.html @@ -61,16 +61,16 @@ - +
Waiting
- + - +
Start Recording
-
End Recording
+
End Recording
@@ -85,7 +85,7 @@ - \ No newline at end of file + diff --git a/views/index.ejs b/views/index.ejs index 07439fa..62fbaf2 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -11,6 +11,59 @@ + -