From 7dfb9f19676a38e835cfeada2809d598b64cbe41 Mon Sep 17 00:00:00 2001 From: redPanther Date: Sun, 12 Jun 2016 22:27:24 +0200 Subject: [PATCH] integrated webserver ... (#697) * initial commit of webconfig * update example config with webconfig and fix format of file update debian postinst script for install example config --- CMakeLists.txt | 6 +- HyperionConfig.h.in | 1 + assets/webconfig/css/index.css | 609 +++++ assets/webconfig/index.html | 69 + .../js/app/api/ChromeLocalStorage.js | 37 + assets/webconfig/js/app/api/ChromeNetwork.js | 57 + .../webconfig/js/app/api/ChromeTcpSocket.js | 307 +++ assets/webconfig/js/app/api/LocalStorage.js | 49 + assets/webconfig/js/app/api/Network.js | 57 + assets/webconfig/js/app/api/Socket.js | 68 + assets/webconfig/js/app/api/WebSocket.js | 229 ++ .../js/app/controllers/AppController.js | 534 +++++ assets/webconfig/js/app/data/ServerControl.js | 201 ++ assets/webconfig/js/app/main.js | 150 ++ assets/webconfig/js/app/main_chrome.js | 108 + assets/webconfig/js/app/models/Settings.js | 124 + assets/webconfig/js/app/utils/Tools.js | 56 + assets/webconfig/js/app/views/EffectsView.js | 64 + assets/webconfig/js/app/views/MainView.js | 396 ++++ assets/webconfig/js/app/views/ServerList.js | 199 ++ assets/webconfig/js/app/views/SettingsView.js | 259 +++ assets/webconfig/js/app/views/Slider.js | 134 ++ .../webconfig/js/app/views/TransformView.js | 375 +++ assets/webconfig/js/background.js | 17 + assets/webconfig/js/vendor/require.js | 2054 +++++++++++++++++ assets/webconfig/js/vendor/stapes.js | 594 +++++ assets/webconfig/js/vendor/tinycolor.js | 1107 +++++++++ assets/webconfig/manifest.json | 32 + assets/webconfig/manifest.webapp | 14 + assets/webconfig/res/colorwheel.png | Bin 0 -> 498530 bytes assets/webconfig/res/fontello.ttf | Bin 0 -> 7876 bytes assets/webconfig/res/fontello.woff | Bin 0 -> 4772 bytes assets/webconfig/res/icon_128.png | Bin 0 -> 17722 bytes cmake/debian/postinst | 2 +- config/hyperion.config.json.example | 153 +- include/webconfig/WebConfig.h | 27 + libsrc/CMakeLists.txt | 4 + .../BoblightClientConnection.cpp | 1 + libsrc/effectengine/EffectEngine.cpp | 1 + libsrc/webconfig/CMakeLists.txt | 53 + libsrc/webconfig/QtHttpClientWrapper.cpp | 221 ++ libsrc/webconfig/QtHttpClientWrapper.h | 51 + libsrc/webconfig/QtHttpHeader.cpp | 32 + libsrc/webconfig/QtHttpHeader.h | 37 + libsrc/webconfig/QtHttpReply.cpp | 75 + libsrc/webconfig/QtHttpReply.h | 55 + libsrc/webconfig/QtHttpRequest.cpp | 60 + libsrc/webconfig/QtHttpRequest.h | 40 + libsrc/webconfig/QtHttpServer.cpp | 66 + libsrc/webconfig/QtHttpServer.h | 48 + libsrc/webconfig/StaticFileServing.cpp | 87 + libsrc/webconfig/StaticFileServing.h | 31 + libsrc/webconfig/WebConfig.cpp | 33 + src/hyperiond/CMakeLists.txt | 7 +- src/hyperiond/hyperiond.cpp | 37 +- 55 files changed, 8952 insertions(+), 76 deletions(-) create mode 100644 assets/webconfig/css/index.css create mode 100644 assets/webconfig/index.html create mode 100644 assets/webconfig/js/app/api/ChromeLocalStorage.js create mode 100644 assets/webconfig/js/app/api/ChromeNetwork.js create mode 100644 assets/webconfig/js/app/api/ChromeTcpSocket.js create mode 100644 assets/webconfig/js/app/api/LocalStorage.js create mode 100644 assets/webconfig/js/app/api/Network.js create mode 100644 assets/webconfig/js/app/api/Socket.js create mode 100644 assets/webconfig/js/app/api/WebSocket.js create mode 100644 assets/webconfig/js/app/controllers/AppController.js create mode 100644 assets/webconfig/js/app/data/ServerControl.js create mode 100644 assets/webconfig/js/app/main.js create mode 100644 assets/webconfig/js/app/main_chrome.js create mode 100644 assets/webconfig/js/app/models/Settings.js create mode 100644 assets/webconfig/js/app/utils/Tools.js create mode 100644 assets/webconfig/js/app/views/EffectsView.js create mode 100644 assets/webconfig/js/app/views/MainView.js create mode 100644 assets/webconfig/js/app/views/ServerList.js create mode 100644 assets/webconfig/js/app/views/SettingsView.js create mode 100644 assets/webconfig/js/app/views/Slider.js create mode 100644 assets/webconfig/js/app/views/TransformView.js create mode 100644 assets/webconfig/js/background.js create mode 100644 assets/webconfig/js/vendor/require.js create mode 100644 assets/webconfig/js/vendor/stapes.js create mode 100644 assets/webconfig/js/vendor/tinycolor.js create mode 100644 assets/webconfig/manifest.json create mode 100644 assets/webconfig/manifest.webapp create mode 100644 assets/webconfig/res/colorwheel.png create mode 100644 assets/webconfig/res/fontello.ttf create mode 100644 assets/webconfig/res/fontello.woff create mode 100644 assets/webconfig/res/icon_128.png create mode 100644 include/webconfig/WebConfig.h create mode 100644 libsrc/webconfig/CMakeLists.txt create mode 100644 libsrc/webconfig/QtHttpClientWrapper.cpp create mode 100644 libsrc/webconfig/QtHttpClientWrapper.h create mode 100644 libsrc/webconfig/QtHttpHeader.cpp create mode 100644 libsrc/webconfig/QtHttpHeader.h create mode 100644 libsrc/webconfig/QtHttpReply.cpp create mode 100644 libsrc/webconfig/QtHttpReply.h create mode 100644 libsrc/webconfig/QtHttpRequest.cpp create mode 100644 libsrc/webconfig/QtHttpRequest.h create mode 100644 libsrc/webconfig/QtHttpServer.cpp create mode 100644 libsrc/webconfig/QtHttpServer.h create mode 100644 libsrc/webconfig/StaticFileServing.cpp create mode 100644 libsrc/webconfig/StaticFileServing.h create mode 100644 libsrc/webconfig/WebConfig.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 388aab0..f55403b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,10 +116,10 @@ find_package(GitVersion) configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h") include_directories("${PROJECT_BINARY_DIR}") -if(ENABLE_QT5) - ADD_DEFINITIONS ( -DENABLE_QT5 ) +if( NOT ENABLE_QT5) + #ADD_DEFINITIONS ( -DENABLE_QT5 ) #find_package(Qt5Widgets) -else() +#else() # Add specific cmake modules to find qt4 (default version finds first available QT which might not be qt4) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/qt4) endif() diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 3ce776b..6f9abff 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -32,6 +32,7 @@ // Define to enable profiler for development purpose #cmakedefine ENABLE_PROFILER +#cmakedefine ENABLE_QT5 // the hyperion build id string #define HYPERION_VERSION_ID "${HYPERION_VERSION_ID}" diff --git a/assets/webconfig/css/index.css b/assets/webconfig/css/index.css new file mode 100644 index 0000000..89d347e --- /dev/null +++ b/assets/webconfig/css/index.css @@ -0,0 +1,609 @@ +@font-face { + font-family: 'fontello'; + src: url('../res/fontello.ttf') format('truetype'), url('../res/fontello.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +html, body { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + color: #A6B4B4; + background-color: #2C2C2C; +} + +* { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -webkit-touch-callout: none; + -webkit-text-size-adjust: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-family: 'Lucida Grande', Helvetica, Arial, Roboto, serif; +} + +#app { + height: 100%; + width: 100%; + display: -webkit-flex; + -webkit-flex-direction: column; + -webkit-flex-wrap: nowrap; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + background-color: #2C2C2C; + min-width: 320px; + min-height: 460px; +} + +.work { + -webkit-flex: 1; + flex: 1; + position: relative; + overflow: hidden; +} + +.footer { + border-top: 1px solid #919F9F; + display: -webkit-flex; + -webkit-justify-content: space-around; + display: flex; + justify-content: space-around; +} + +.footer .button { + width: 100%; + text-align: center; + position: relative; + padding: 5px; +} + +.touchrect { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 10; +} + +.icon { + font-family: "fontello"; +} + +.footer .button .icon { + font-size: 25px; + margin-top: 8px; +} + +.footer .button .title { + font-size: 12px; + margin-top: 8px; + margin-bottom: 8px; +} + +.footer .button:active, +.footer .button.selected { + color: #FFFFFF; +} + +.container { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 400%; + display: flex; + display: -webkit-flex; + -webkit-transition: left 0.5s ease-in-out; + transition: left 0.5s ease-in-out; +} + +.contentarea { + width: 100%; + height: 100%; + overflow-y: auto; + -ms-overflow-y: auto; + -ms-overflow-style: -ms-autohiding-scrollbar; + position: relative; + -webkit-overflow-scrolling: touch; +} + +#color { + display: -webkit-flex; + -webkit-flex-direction: column; + display: flex; + flex-direction: column; +} + +#colorpicker { + position: relative; + -webkit-flex: 1; + flex: 1; +} + +#colorwheelbg { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; +} + +#pointer { + width: 30px; + height: 30px; + position: absolute; + border: 2px solid #FFFFFF; + border-radius: 15px; + left: calc(50% - 15px); + top: calc(50% - 15px); +} + +ul { + list-style: none; + padding: 0; + margin: 0; +} + +li { + display: block; +} + +.horizontal { + display: -webkit-flex; + -webkit-flex: 1; + display: flex; + flex: 1; + position: relative; +} + +li .delete_icon { + font-family: "fontello"; + font-size: 20px; + color: red; + height: 40px; + width: 40px; + line-height: 40px; + text-align: center; +} + +li .edit_icon { + font-family: "fontello"; + font-size: 20px; + height: 40px; + width: 40px; + line-height: 40px; + text-align: center; +} + +.locked .edit_icon, +.locked .delete_icon { + display: none; +} + +li .titlebox { + -webkit-flex: 1; + flex: 1; +} + +.titlebox label { + display: block; + line-height: 20px; + height: 20px; +} + +.titlebox label.title { + font-size: 16px; +} + +.titlebox label.subtitle { + font-size: 10px; +} + +/* +li:not(:last-child) { + border-bottom: 1px solid #919F9F; +} +*/ + +li { + border-bottom: 1px solid #919F9F; +} + +li input[type=range] { + width: 100%; +} + +#transform li { + padding: 0; +} + +#transform .icon { + width: 40px; + text-align: center; +} + +.group, +.grouplist { + margin-top: 10px; +} + +.grouplist .header { + display: flex; + justify-content: space-between; + border-bottom: 1px solid gray; + align-items: center; + padding-left: 15px; + font-size: 16px; + display: -webkit-flex; + -webkit-justify-content: space-between; + -webkit-align-items: center; +} + +.group ul, +.grouplist ul { + overflow-y: hidden; + -webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; +} + +.group[collapsed=true] ul { + max-height: 0; +} + +.group li label { + margin: 4px 0 0 50px; + display: block; + background: none; +} + +.group > .header { + border-bottom: 1px solid gray; + padding: 3px 0 3px 10px; + position: relative; +} + +.group > .header label { + display: block; + background: none; +} + +.group > .header label:first-child { + font-size: 16px; +} + +.group > .header label:last-child { + font-size: 12px; +} + +.group > .header label:first-child:after { + font-family: "fontello"; + content: '\e808'; + font-size: 20px; + transition: all 0.5s ease-in-out; + -webkit-transform-origin: 50% 33%; + -webkit-transform: rotateZ(180deg); + transform-origin: 50% 33%; + transform: rotateZ(180deg); + -ms-transform-origin: 50% 33%; + -ms-transform: rotateZ(180deg); + position: absolute; + right: 24px; + top: 0; + bottom: 0; +} + +.grouplist .header .callout { + font-family: "fontello"; + font-size: 20px; + padding: 10px; +} + +.grouplist .header .callout:active { + color: #FFFFFF; +} + +.group[collapsed=true] > .header label:first-child:after { + -webkit-transform: rotateZ(0deg); + -ms-transform: rotateZ(0deg); + transform: rotateZ(0deg); +} + +.wrapper { + display: flex; + align-items: center; + display: -webkit-flex; + -webkit-align-items: center; +} + +.wrapper .value { + width: 40px; + background: transparent; + border: 0; + color: #FFFFFF; + margin: auto 5px; +} + +#transform .group .slider { + -webkit-flex: 1; + flex: 1; + width: 100%; +} + +.slider { + position: relative; + margin-left: 20px; + margin-right: 20px; +} + +.slider .track { + border-radius: 4px; + height: 4px; + border: 1px solid #BDC3C7; + background-color: #FFFFFF; + +} + +.slider .thumb { + box-sizing: border-box; + border-radius: 10px; + height: 20px; + width: 20px; + border: 2px solid #BDC3C7; + background-color: #FFFFFF; + position: absolute; + top: -8px; + margin-left: -10px; +} + +.wrapper .icon { + display: block; + font-size: 30px; +} + +.red .icon { + color: #FF0000; +} + +.green .icon { + color: #00FF00; +} + +.blue .icon { + color: #0000FF; +} + +.msg { + position: relative; + margin: auto; + padding: 5px 10px; + font-size: 12px; + font-weight: bold; +} + +.error { + background-color: #D70000; + color: #FFFFFF; +} + +.status { + background-color: #00A200; + color: #FFFFFF; +} + +.wrapper_msg { + position: absolute; + bottom: 5px; + width: 100%; + display: -webkit-flex; + display: flex; + visibility: visible; + opacity: 1; + -webkit-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} + +.invisible { + opacity: 0; + visibility: hidden; +} + +.hidden { + display: none !important; +} + +.inputline { + display: -webkit-flex; + -webkit-justify-content: space-between; + -webkit-align-items: center; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + padding: 6px 10px; +} + +#settings .inputline input { + background: none; + border: 1px solid rgba(100, 100, 100, 0.4); + height: 100%; + color: white; + text-align: right; + font-size: inherit; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + outline: none; +} + +#settings .inputline input:focus { + color: white; +} + +.line { + width: 100%; + position: relative; + padding: 5px 0; +} + +button { + margin: auto 10px; + border: 1px #A6B4B4 solid; + background: none; + color: #A6B4B4; + padding: 6px 20px; +} + +button:active { + border: 1px #FFFFFF solid; + background-color: transparent; + color: #FFFFFF; + outline: none; +} + +button:focus { + outline: none; +} + +.spinner { + height: 20px; + width: 20px; + display: inline-flex; + display: -webkit-inline-flex; + -webkit-animation: rotation .8s infinite linear; + animation: rotation .8s infinite linear; + border: 6px inset #D7D7D7; + border-radius: 50%; + float: right; + margin-right: 15px; +} + +@-webkit-keyframes rotation { + from { + -ms-transform: rotate(0deg); + } + to { + -ms-transform: rotate(359deg); + } +} + +@-webkit-keyframes rotation { + from { + -webkit-transform: rotate(0deg); + } + to { + -webkit-transform: rotate(359deg); + } +} + +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } +} + +li.selected { + background-color: rgba(100, 100, 100, 0.5); +} + +#effects li { + height: 40px; + line-height: 40px; + padding-left: 20px; +} + +.info { + font-size: 16px; + position: absolute; + top: 50%; + width: 100%; + text-align: center; +} + +#color #buttonctrl { + display: -webkit-flex; + -webkit-justify-content: space-between; + -webkit-align-items: flex-end; + display: flex; + justify-content: space-between; + align-items: flex-end; + margin: 10px auto 35px auto; +} + +#color #buttonctrl > .icon { + font-size: 27px; + line-height: 100%; + padding: 0 20px; +} + +#color #buttonctrl > .icon:active { + color: #FFFFFF; +} + +#color .slider { + position: relative; + width: 70%; + margin: 10px auto; + height: 20px; +} + +#color .slider .track { + width: 100%; + height: 100%; + background-image: -webkit-linear-gradient(left, #000000 0%, #FFFFFF 100%); + background-image: linear-gradient(to right, #000000 0%, #FFFFFF 100%); +} + +#color .slider .thumb { + background: transparent; + border: 3px solid rgba(255, 255, 255, 1.0); + width: 14px; + border-radius: 4px; + height: 28px; + position: absolute; + top: -4px; + margin-left: -7px; +} + +#color .value { + outline: none; + border: 1px solid white; + font-family: Monaco, monospace; + text-align: center; + background: transparent; + width: 100px; + color: white; + font-size: 16px; + display: block; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.checkbox::before { + display: table-cell; + font-family: "fontello"; + content: '\e803'; + height: 40px; + width: 40px; + font-size: 20px; + vertical-align: middle; + text-align: center; +} + +.selected .checkbox::before { + content: '\e802'; +} \ No newline at end of file diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html new file mode 100644 index 0000000..5f28371 --- /dev/null +++ b/assets/webconfig/index.html @@ -0,0 +1,69 @@ + + + + Hyperion remote control + + + + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/assets/webconfig/js/app/api/ChromeLocalStorage.js b/assets/webconfig/js/app/api/ChromeLocalStorage.js new file mode 100644 index 0000000..82467df --- /dev/null +++ b/assets/webconfig/js/app/api/ChromeLocalStorage.js @@ -0,0 +1,37 @@ +/*global define, chrome */ +define(['api/LocalStorage'], function (LocalStorage) { + 'use strict'; + return LocalStorage.subclass(/** @lends ChromeLocalStorage.prototype */{ + + /** + * @class ChromeLocalStorage + * @classdesc Chrome's persistent storage + * @constructs + * @extends LocalStorage + */ + constructor: function () { + }, + + get: function () { + chrome.storage.local.get('data', function (entry) { + if (chrome.runtime.lastError) { + this.emit('error', chrome.runtime.lastError.message); + } else { + this.emit('got', entry.data); + } + }.bind(this)); + }, + + set: function (data) { + var entry = {}; + entry.data = data; + chrome.storage.local.set(entry, function () { + if (chrome.runtime.lastError) { + this.emit('error', chrome.runtime.lastError.message); + } else { + this.emit('set'); + } + }.bind(this)); + } + }); +}); diff --git a/assets/webconfig/js/app/api/ChromeNetwork.js b/assets/webconfig/js/app/api/ChromeNetwork.js new file mode 100644 index 0000000..9915411 --- /dev/null +++ b/assets/webconfig/js/app/api/ChromeNetwork.js @@ -0,0 +1,57 @@ +/*global chrome */ +define(['api/Network'], function (Network) { + 'use strict'; + + return Network.subclass(/** @lends ChromeNetwork.prototype */{ + + /** + * @class ChromeNetwork + * @extends Network + * @classdesc Network functions for chrome apps + * @constructs + */ + constructor: function () { + }, + + /** + * @overrides + * @param onSuccess + * @param onError + */ + getLocalInterfaces: function (onSuccess, onError) { + var ips = []; + + chrome.system.network.getNetworkInterfaces(function (networkInterfaces) { + var i; + + if (chrome.runtime.lastError) { + if (onError) { + onError('Could not get network interfaces'); + } + return; + } + + for (i = 0; i < networkInterfaces.length; i++) { + // check only ipv4 + if (networkInterfaces[i].address.indexOf('.') === -1) { + continue; + } + + ips.push(networkInterfaces[i].address); + } + + if (onSuccess) { + onSuccess(ips); + } + }); + }, + + /** + * @overrides + * @return {boolean} + */ + canDetectLocalAddress: function () { + return true; + } + }, true); +}); diff --git a/assets/webconfig/js/app/api/ChromeTcpSocket.js b/assets/webconfig/js/app/api/ChromeTcpSocket.js new file mode 100644 index 0000000..f626b1d --- /dev/null +++ b/assets/webconfig/js/app/api/ChromeTcpSocket.js @@ -0,0 +1,307 @@ +/*global chrome */ +define(['lib/stapes', 'api/Socket', 'utils/Tools'], function (Stapes, Socket, tools) { + 'use strict'; + return Socket.subclass(/** @lends ChromeTcpSocket.prototype */{ + DEBUG: false, + + /** + * @type {number} + */ + handle: null, + + /** + * @type {function} + */ + currentResponseCallback: null, + + /** + * @type {function} + */ + currentErrorCallback: null, + + /** + * Temporary buffer for incoming data + * @type {Uint8Array} + */ + inputBuffer: null, + inputBufferIndex: 0, + readBufferTimerId: null, + + /** + * @class ChromeTcpSocket + * @extends Socket + * @constructs + */ + constructor: function () { + this.inputBuffer = new Uint8Array(4096); + this.inputBufferIndex = 0; + + chrome.sockets.tcp.onReceive.addListener(this.onDataReceived.bind(this)); + chrome.sockets.tcp.onReceiveError.addListener(this.onError.bind(this)); + }, + + create: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] Creating socket...'); + } + chrome.sockets.tcp.create({bufferSize: 4096}, function (createInfo) { + if (this.DEBUG) { + console.log('[DEBUG] Socket created: ' + createInfo.socketId); + } + this.handle = createInfo.socketId; + if (onSuccess) { + onSuccess(); + } + }.bind(this)); + }, + + isConnected: function (resultCallback) { + if (this.DEBUG) { + console.log('[DEBUG] Checking if socket is connected...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (resultCallback) { + resultCallback(false); + } + return; + } + + chrome.sockets.tcp.getInfo(this.handle, function (socketInfo) { + if (this.DEBUG) { + console.log('[DEBUG] Socket connected: ' + socketInfo.connected); + } + + if (socketInfo.connected) { + if (resultCallback) { + resultCallback(true); + } + } else { + if (resultCallback) { + resultCallback(false); + } + } + }.bind(this)); + }, + + connect: function (server, onSuccess, onError) { + var timeoutHandle; + + if (this.DEBUG) { + console.log('[DEBUG] Connecting to peer ' + server.address + ':' + server.port); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + return; + } + + // FIXME for some reason chrome blocks if peer is not reachable + timeoutHandle = setTimeout(function () { + chrome.sockets.tcp.getInfo(this.handle, function (socketInfo) { + if (!socketInfo.connected) { + // let the consumer decide if to close or not? + // this.close(); + onError('Could not connect to ' + server.address + ':' + server.port); + } + }.bind(this)); + }.bind(this), 500); + + chrome.sockets.tcp.connect(this.handle, server.address, server.port, function (result) { + if (this.DEBUG) { + console.log('[DEBUG] Connect result: ' + result); + } + clearTimeout(timeoutHandle); + + if (chrome.runtime.lastError) { + if (onError) { + onError('Could not connect to ' + server.address + ':' + server.port); + } + return; + } + + if (result !== 0) { + if (onError) { + onError('Could not connect to ' + server.address + ':' + server.port); + } + } else if (onSuccess) { + onSuccess(); + } + }.bind(this)); + }, + + close: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] Closing socket...'); + } + + if (this.handle) { + chrome.sockets.tcp.close(this.handle, function () { + this.handle = null; + if (onSuccess) { + onSuccess(); + } + }.bind(this)); + } else { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + } + }, + + write: function (data, onSuccess, onError) { + var dataToSend = null, dataType = typeof (data); + + if (this.DEBUG) { + console.log('[DEBUG] writing to socket...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + return; + } + + this.isConnected(function (connected) { + if (connected) { + if (dataType === 'string') { + if (this.DEBUG) { + console.log('> ' + data); + } + dataToSend = tools.str2ab(data); + } else { + if (this.DEBUG) { + console.log('> ' + tools.ab2hexstr(data)); + } + dataToSend = data; + } + + chrome.sockets.tcp.send(this.handle, tools.a2ab(dataToSend), function (sendInfo) { + if (this.DEBUG) { + console.log('[DEBUG] Socket write result: ' + sendInfo.resultCode); + } + + if (sendInfo.resultCode !== 0) { + onError('Socket write error: ' + sendInfo.resultCode); + } else if (onSuccess) { + onSuccess(); + } + }.bind(this)); + } else { + if (onError) { + onError('No connection to peer'); + } + } + }.bind(this)); + + }, + + read: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] reading from socket...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + return; + } + + this.isConnected(function (connected) { + if (!connected) { + this.currentResponseCallback = null; + this.currentErrorCallback = null; + + if (onError) { + onError('No connection to peer'); + } + } + }.bind(this)); + + if (onSuccess) { + this.currentResponseCallback = onSuccess; + } + + if (onError) { + this.currentErrorCallback = onError; + } + }, + + /** + * Data receiption callback + * @private + * @param info + */ + onDataReceived: function (info) { + if (this.DEBUG) { + console.log('[DEBUG] received data...'); + } + + if (info.socketId === this.handle && info.data) { + if (this.readBufferTimerId) { + clearTimeout(this.readBufferTimerId); + } + if (this.readTimeoutTimerId) { + clearTimeout(this.readTimeoutTimerId); + this.readTimeoutTimerId = null; + } + this.inputBuffer.set(new Uint8Array(info.data), this.inputBufferIndex); + this.inputBufferIndex += info.data.byteLength; + + if (this.DEBUG) { + console.log('< ' + tools.ab2hexstr(info.data)); + } + + if (this.currentResponseCallback) { + this.readBufferTimerId = setTimeout(function () { + this.currentResponseCallback(this.inputBuffer.subarray(0, this.inputBufferIndex)); + this.inputBufferIndex = 0; + this.currentResponseCallback = null; + }.bind(this), 200); + } + } + }, + + /** + * Error callback + * @private + * @param info + */ + onError: function (info) { + if (this.DEBUG) { + console.log('[ERROR]: ' + info.resultCode); + } + + if (info.socketId === this.handle) { + if (this.currentErrorCallback) { + this.currentErrorCallback(info.resultCode); + this.currentErrorCallback = null; + } + } + } + }, true); +}); diff --git a/assets/webconfig/js/app/api/LocalStorage.js b/assets/webconfig/js/app/api/LocalStorage.js new file mode 100644 index 0000000..1b74543 --- /dev/null +++ b/assets/webconfig/js/app/api/LocalStorage.js @@ -0,0 +1,49 @@ +/*global define */ +define(['lib/stapes'], function (Stapes) { + 'use strict'; + return Stapes.subclass(/** @lends LocalStorage.prototype */{ + + /** + * @class LocalStorage + * @classdesc LocalStorage handler using HTML5 localStorage + * @constructs + * + * @fires got + * @fires error + * @fires set + */ + constructor: function () { + }, + + /** + * Gets stored data + */ + get: function () { + var data; + + if (!window.localStorage) { + this.emit('error', 'Local Storage not supported'); + return; + } + + if (localStorage.data) { + data = JSON.parse(localStorage.data); + this.emit('got', data); + } + }, + + /** + * Stores settings + * @param {object} data - Data object to store + */ + set: function (data) { + if (!window.localStorage) { + this.emit('error', 'Local Storage not supported'); + return; + } + + localStorage.data = JSON.stringify(data); + this.emit('set'); + } + }); +}); diff --git a/assets/webconfig/js/app/api/Network.js b/assets/webconfig/js/app/api/Network.js new file mode 100644 index 0000000..2d93ca6 --- /dev/null +++ b/assets/webconfig/js/app/api/Network.js @@ -0,0 +1,57 @@ +/*global define */ +define(['lib/stapes'], function (Stapes) { + 'use strict'; + return Stapes.subclass(/** @lends Network.prototype */{ + detectTimerId: null, + + /** + * @class Network + * @classdesc Empty network functions handler + * @constructs + */ + constructor: function () { + }, + + /** + * Returns the list of known local interfaces (ipv4) + * @param {function(string[])} [onSuccess] - Callback to call on success + * @param {function(error:string)} [onError] - Callback to call on error + */ + getLocalInterfaces: function (onSuccess, onError) { + var ips = [], RTCPeerConnection; + + // https://developer.mozilla.org/de/docs/Web/API/RTCPeerConnection + RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection; + + var rtc = new RTCPeerConnection({iceServers: []}); + rtc.onicecandidate = function (event) { + var parts; + + if (this.detectTimerId) { + clearTimeout(this.detectTimerId); + } + + if (event.candidate) { + parts = event.candidate.candidate.split(' '); + if (ips.indexOf(parts[4]) === -1) { + console.log(event.candidate); + ips.push(parts[4]); + } + } + + this.detectTimerId = setTimeout(function () { + if (onSuccess) { + onSuccess(ips); + } + }, 200); + }.bind(this); + + rtc.createDataChannel(''); + rtc.createOffer(rtc.setLocalDescription.bind(rtc), onError); + }, + + canDetectLocalAddress: function () { + return window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection; + } + }, true); +}); diff --git a/assets/webconfig/js/app/api/Socket.js b/assets/webconfig/js/app/api/Socket.js new file mode 100644 index 0000000..1d6e587 --- /dev/null +++ b/assets/webconfig/js/app/api/Socket.js @@ -0,0 +1,68 @@ +/*global define */ +define(['lib/stapes'], function (Stapes) { + 'use strict'; + return Stapes.subclass(/** @lends Socket.prototype */{ + + /** + * @class Socket + * @abstract + */ + constructor: function () { + }, + + /** + * Create the socket + * @param onSuccess + * @param onError + * @abstract + */ + create: function (onSuccess, onError) { + }, + + /** + * Check if a connection is opened. + * @abstract + */ + isConnected: function () { + return false; + }, + + /** + * Connect to another peer + * @param {Object} peer Port object + * @param {function} [onSuccess] Callback to call on success + * @param {function(error:string)} [onError] Callback to call on error + * @abstract + */ + connect: function (peer, onSuccess, onError) { + }, + + /** + * Close the current connection + * @param {function} [onSuccess] Callback to call on success + * @param {function(error:string)} [onError] Callback to call on error + * @abstract + */ + close: function (onSuccess, onError) { + }, + + /** + * Read data from the socket + * @param {function} [onSuccess] Callback to call on success + * @param {function(error:string)} [onError] Callback to call on error + * @abstract + */ + read: function (onSuccess, onError) { + }, + + /** + * Writes data to the socket + * @param {string | Array} data Data to send. + * @param {function} [onSuccess] Callback to call if data was sent successfully + * @param {function(error:string)} [onError] Callback to call on error + * @abstract + */ + write: function (data, onSuccess, onError) { + } + }, true); +}); diff --git a/assets/webconfig/js/app/api/WebSocket.js b/assets/webconfig/js/app/api/WebSocket.js new file mode 100644 index 0000000..a231f07 --- /dev/null +++ b/assets/webconfig/js/app/api/WebSocket.js @@ -0,0 +1,229 @@ +define(['lib/stapes', 'api/Socket', 'utils/Tools'], function (Stapes, Socket, tools) { + 'use strict'; + return Socket.subclass(/** @lends WebSocket.prototype */{ + DEBUG: false, + + handle: null, + + /** + * @type {function} + */ + currentResponseCallback: null, + + /** + * @type {function} + */ + currentErrorCallback: null, + + /** + * Temporary buffer for incoming data + * @type {Uint8Array} + */ + inputBuffer: null, + inputBufferIndex: 0, + readBufferTimerId: null, + + /** + * @class WebSocket + * @extends Socket + * @constructs + */ + constructor: function () { + this.inputBuffer = new Uint8Array(4096); + this.inputBufferIndex = 0; + }, + + create: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] Creating socket...'); + } + + if (onSuccess) { + onSuccess(); + } + }, + + isConnected: function (resultCallback) { + if (this.DEBUG) { + console.log('[DEBUG] Checking if socket is connected...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (resultCallback) { + resultCallback(false); + } + return; + } + + if (resultCallback) { + if (this.handle.readyState === WebSocket.OPEN) { + resultCallback(true); + } else { + resultCallback(false); + } + } + }, + + connect: function (server, onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] Connecting to peer ' + server.address + ':' + server.port); + } + + this.currentErrorCallback = onError; + + this.handle = new WebSocket('ws://' + server.address + ':' + server.port); + this.handle.onmessage = this.onDataReceived.bind(this); + this.handle.onclose = function () { + if (this.DEBUG) { + console.log('onClose'); + } + }.bind(this); + this.handle.onerror = function () { + if (this.DEBUG) { + console.log('[ERROR]: '); + } + + if (this.currentErrorCallback) { + this.currentErrorCallback('WebSocket error'); + this.currentErrorCallback = null; + } + }.bind(this); + this.handle.onopen = function () { + if (onSuccess) { + onSuccess(); + } + }; + }, + + close: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] Closing socket...'); + } + + if (this.handle) { + this.handle.close(); + if (onSuccess) { + onSuccess(); + } + } else { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + } + }, + + write: function (data, onSuccess, onError) { + var dataToSend = null, dataType = typeof (data); + + if (this.DEBUG) { + console.log('[DEBUG] writing to socket...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] Socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + return; + } + + this.isConnected(function (connected) { + if (connected) { + if (dataType === 'string') { + if (this.DEBUG) { + console.log('> ' + data); + } + //dataToSend = tools.str2ab(data); + dataToSend = data; + } else { + if (this.DEBUG) { + console.log('> ' + tools.ab2hexstr(data)); + } + dataToSend = data; + } + + this.currentErrorCallback = onError; + this.handle.send(dataToSend); + + if (onSuccess) { + onSuccess(); + } + } else { + if (onError) { + onError('No connection to peer'); + } + } + }.bind(this)); + + }, + + read: function (onSuccess, onError) { + if (this.DEBUG) { + console.log('[DEBUG] reading from socket...'); + } + + if (!this.handle) { + if (this.DEBUG) { + console.log('[DEBUG] socket not created'); + } + + if (onError) { + onError('Socket handle is invalid'); + } + return; + } + + this.isConnected(function (connected) { + if (!connected) { + this.currentResponseCallback = null; + this.currentErrorCallback = null; + + if (onError) { + onError('No connection to peer'); + } + } + }.bind(this)); + + if (onSuccess) { + this.currentResponseCallback = onSuccess; + } + + if (onError) { + this.currentErrorCallback = onError; + } + }, + + /** + * Data receiption callback + * @private + * @param event + */ + onDataReceived: function (event) { + if (this.DEBUG) { + console.log('[DEBUG] received data...'); + } + + if (this.handle && event.data) { + if (this.DEBUG) { + console.log('< ' + event.data); + } + + if (this.currentResponseCallback) { + this.currentResponseCallback(tools.str2ab(event.data)); + this.currentResponseCallback = null; + } + } + } + }, true); +}); diff --git a/assets/webconfig/js/app/controllers/AppController.js b/assets/webconfig/js/app/controllers/AppController.js new file mode 100644 index 0000000..c50e4e6 --- /dev/null +++ b/assets/webconfig/js/app/controllers/AppController.js @@ -0,0 +1,534 @@ +/*global define */ +define([ + 'lib/stapes', 'views/MainView', 'models/Settings', 'views/SettingsView', 'views/EffectsView', 'views/TransformView', 'data/ServerControl', 'api/Socket', 'api/Network' +], function (Stapes, MainView, Settings, SettingsView, EffectsView, TransformView, ServerControl, Socket, Network) { + 'use strict'; + var network = new Network(); + + return Stapes.subclass(/** @lends AppController.prototype */{ + /** + * @type MainView + */ + mainView: null, + /** + * @type SettingsView + */ + settingsView: null, + /** + * @type EffectsView + */ + effectsView: null, + /** + * @type TransformView + */ + transformView: null, + /** + * @type Settings + */ + settings: null, + /** + * @type ServerControl + */ + serverControl: null, + color: { + r: 25, + g: 25, + b: 25 + }, + effects: [], + transform: {}, + selectedServer: null, + + /** + * @class AppController + * @constructs + */ + constructor: function () { + this.mainView = new MainView(); + this.settingsView = new SettingsView(); + this.effectsView = new EffectsView(); + this.transformView = new TransformView(); + + this.settings = new Settings(); + + this.bindEventHandlers(); + this.mainView.setColor(this.color); + + if (!network.canDetectLocalAddress()) { + this.settingsView.enableDetectButton(false); + } + }, + + /** + * Do initialization + */ + init: function () { + this.settings.load(); + }, + + /** + * @private + */ + bindEventHandlers: function () { + this.settings.on({ + 'loaded': function () { + var i; + + for (i = 0; i < this.settings.servers.length; i++) { + if (this.settings.servers[i].selected) { + this.selectedServer = this.settings.servers[i]; + break; + } + } + + this.settingsView.fillServerList(this.settings.servers); + + if (!this.selectedServer) { + this.gotoArea('settings'); + } else { + this.connectToServer(this.selectedServer); + } + }, + 'error': function (message) { + this.showError(message); + }, + 'serverAdded': function (server) { + var i; + for (i = 0; i < this.settings.servers.length; i++) { + if (this.settings.servers[i].selected) { + this.selectedServer = this.settings.servers[i]; + this.connectToServer(server); + break; + } + } + + this.settingsView.fillServerList(this.settings.servers); + }, + 'serverChanged': function (server) { + var i; + for (i = 0; i < this.settings.servers.length; i++) { + if (this.settings.servers[i].selected) { + this.selectedServer = this.settings.servers[i]; + this.connectToServer(server); + break; + } + } + + this.settingsView.fillServerList(this.settings.servers); + this.connectToServer(server); + }, + 'serverRemoved': function () { + var i, removedSelected = true; + this.settingsView.fillServerList(this.settings.servers); + + for (i = 0; i < this.settings.servers.length; i++) { + if (this.settings.servers[i].selected) { + removedSelected = false; + break; + } + } + + if (removedSelected) { + this.selectedServer = null; + if (this.serverControl) { + this.serverControl.disconnect(); + } + this.effectsView.clear(); + this.transformView.clear(); + } + } + }, this); + + this.mainView.on({ + 'barClick': function (id) { + if (id !== 'settings') { + if (!this.selectedServer) { + this.showError('No server selected'); + } else if (!this.serverControl) { + this.connectToServer(this.selectedServer); + } + } + this.gotoArea(id); + }, + 'colorChange': function (color) { + this.color = color; + + if (!this.selectedServer) { + this.showError('No server selected'); + } else if (!this.serverControl) { + this.connectToServer(this.selectedServer, function () { + this.serverControl.setColor(color, this.selectedServer.duration); + }.bind(this)); + } else { + this.serverControl.setColor(color, this.selectedServer.duration); + } + }, + 'clear': function () { + if (!this.selectedServer) { + this.showError('No server selected'); + } else if (!this.serverControl) { + this.connectToServer(this.selectedServer, function () { + this.serverControl.clear(); + }.bind(this)); + } else { + this.serverControl.clear(); + } + }, + 'clearall': function () { + if (!this.selectedServer) { + this.showError('No server selected'); + } else if (!this.serverControl) { + this.connectToServer(this.selectedServer, function () { + this.serverControl.clearall(); + this.mainView.setColor({r: 0, g: 0, b: 0}); + }.bind(this)); + } else { + this.serverControl.clearall(); + this.mainView.setColor({r: 0, g: 0, b: 0}); + } + } + }, this); + + this.settingsView.on({ + 'serverAdded': function (server) { + if (server.address && server.port) { + server.priority = server.priority || 50; + this.settings.addServer(server); + this.lockSettingsView(false); + } else { + this.showError('Invalid server data'); + } + }, + 'serverAddCanceled': function () { + this.lockSettingsView(false); + this.settingsView.fillServerList(this.settings.servers); + }, + 'serverEditCanceled': function () { + this.lockSettingsView(false); + this.settingsView.fillServerList(this.settings.servers); + }, + 'serverSelected': function (index) { + this.lockSettingsView(false); + this.settings.setSelectedServer(index); + }, + 'serverRemoved': function (index) { + this.settings.removeServer(index); + }, + 'serverChanged': function (data) { + if (data.server.address && data.server.port) { + data.server.priority = data.server.priority || 50; + this.settings.updateServer(data.index, data.server); + this.lockSettingsView(false); + } else { + this.showError('Invalid server data'); + } + }, + 'editServer': function (index) { + var server = this.settings.servers[index]; + this.settingsView.editServer({index: index, server: server}); + }, + 'durationChanged': function (value) { + this.settings.duration = value; + this.settings.save(); + }, + 'detect': function () { + this.lockSettingsView(true); + this.settingsView.showWaiting(true); + this.searchForServer(function (server) { + this.settings.addServer(server); + }.bind(this), function () { + this.lockSettingsView(false); + this.settingsView.showWaiting(false); + }.bind(this)); + } + }, this); + + this.effectsView.on({ + 'effectSelected': function (effectId) { + if (!this.serverControl) { + this.connectToServer(this.selectedServer, function () { + this.serverControl.runEffect(this.effects[parseInt(effectId)]); + }.bind(this)); + } else { + this.serverControl.runEffect(this.effects[parseInt(effectId)]); + } + } + }, this); + + this.transformView.on({ + 'gamma': function (data) { + if (data.r) { + this.transform.gamma[0] = data.r; + } else if (data.g) { + this.transform.gamma[1] = data.g; + } else if (data.b) { + this.transform.gamma[2] = data.b; + } + + if (this.serverControl) { + this.serverControl.setTransform(this.transform); + } + }, + 'whitelevel': function (data) { + if (data.r) { + this.transform.whitelevel[0] = data.r; + } else if (data.g) { + this.transform.whitelevel[1] = data.g; + } else if (data.b) { + this.transform.whitelevel[2] = data.b; + } + + if (this.serverControl) { + this.serverControl.setTransform(this.transform); + } + }, + 'blacklevel': function (data) { + if (data.r) { + this.transform.blacklevel[0] = data.r; + } else if (data.g) { + this.transform.blacklevel[1] = data.g; + } else if (data.b) { + this.transform.blacklevel[2] = data.b; + } + + if (this.serverControl) { + this.serverControl.setTransform(this.transform); + } + }, + 'threshold': function (data) { + if (data.r) { + this.transform.threshold[0] = data.r; + } else if (data.g) { + this.transform.threshold[1] = data.g; + } else if (data.b) { + this.transform.threshold[2] = data.b; + } + + if (this.serverControl) { + this.serverControl.setTransform(this.transform); + } + }, + 'hsv': function (data) { + if (data.valueGain) { + this.transform.valueGain = data.valueGain; + } else if (data.saturationGain) { + this.transform.saturationGain = data.saturationGain; + } + + if (this.serverControl) { + this.serverControl.setTransform(this.transform); + } + } + }, this); + }, + + /** + * @private + * @param id + */ + gotoArea: function (id) { + this.mainView.scrollToArea(id); + }, + + /** + * @private + * @param server + */ + connectToServer: function (server, onConnected) { + if (this.serverControl) { + if (this.serverControl.isConnecting()) { + return; + } + this.serverControl.off(); + this.serverControl.disconnect(); + this.transformView.clear(); + this.effectsView.clear(); + } + + this.serverControl = new ServerControl(server, Socket); + this.serverControl.on({ + connected: function () { + this.serverControl.getServerInfo(); + }, + serverInfo: function (info) { + var index; + if (!this.selectedServer.name || this.selectedServer.name.length === 0) { + this.selectedServer.name = info.hostname; + index = this.settings.indexOfServer(this.selectedServer); + this.settings.updateServer(index, this.selectedServer); + this.settingsView.fillServerList(this.settings.servers); + } + this.effects = info.effects; + this.transform = info.transform[0]; + this.updateView(); + this.showStatus('Connected to ' + this.selectedServer.name); + if (onConnected) { + onConnected(); + } + }, + error: function (message) { + this.serverControl = null; + this.showError(message); + } + }, this); + this.serverControl.connect(); + }, + + /** + * @private + */ + updateView: function () { + var i, effects = []; + if (this.effects) { + for (i = 0; i < this.effects.length; i++) { + effects.push({id: i, name: this.effects[i].name}); + } + } + + this.effectsView.clear(); + this.effectsView.fillList(effects); + + this.transformView.clear(); + this.transformView.fillList(this.transform); + }, + + /** + * Shows the error text + * @param {string} error - Error message + */ + showError: function (error) { + this.mainView.showError(error); + }, + + /** + * Shows a message + * @param {string} message - Text to show + */ + showStatus: function (message) { + this.mainView.showStatus(message); + }, + + /** + * @private + * @param lock + */ + lockSettingsView: function (lock) { + if (network.canDetectLocalAddress()) { + this.settingsView.enableDetectButton(!lock); + } + this.settingsView.lockList(lock); + }, + + /** + * @private + * @param onFound + * @param onEnd + */ + searchForServer: function (onFound, onEnd) { + network.getLocalInterfaces(function (ips) { + if (ips.length === 0) { + onEnd(); + return; + } + + function checkInterface (localInterfaceAddress, ciOnFinished) { + var index, ipParts, addr; + + index = 1; + ipParts = localInterfaceAddress.split('.'); + ipParts[3] = index; + addr = ipParts.join('.'); + + function checkAddressRange (startAddress, count, carOnFinished) { + var ipParts, i, addr, cbCounter = 0, last; + + function checkAddress (address, port, caOnFinished) { + var server = new ServerControl({'address': address, 'port': port}, Socket); + server.on({ + 'error': function () { + server.disconnect(); + caOnFinished(); + }, + 'connected': function () { + server.getServerInfo(); + }, + 'serverInfo': function (result) { + var serverInfo = { + 'address': address, + 'port': port, + 'priority': 50 + }; + server.disconnect(); + + if (result.hostname) { + serverInfo.name = result.hostname; + } + + caOnFinished(serverInfo); + } + }); + server.connect(); + } + + function checkAddressDoneCb (serverInfo) { + var ipParts, nextAddr; + + if (serverInfo && onFound) { + onFound(serverInfo); + } + + cbCounter++; + if (cbCounter === count) { + ipParts = startAddress.split('.'); + ipParts[3] = parseInt(ipParts[3]) + count; + nextAddr = ipParts.join('.'); + carOnFinished(nextAddr); + } + } + + ipParts = startAddress.split('.'); + last = parseInt(ipParts[3]); + + for (i = 0; i < count; i++) { + ipParts[3] = last + i; + addr = ipParts.join('.'); + + checkAddress(addr, 19444, checkAddressDoneCb); + } + } + + function checkAddressRangeCb (nextAddr) { + var ipParts, count = 64, lastPart; + + ipParts = nextAddr.split('.'); + lastPart = parseInt(ipParts[3]); + if (lastPart === 255) { + ciOnFinished(); + return; + } else if (lastPart + 64 > 254) { + count = 255 - lastPart; + } + + checkAddressRange(nextAddr, count, checkAddressRangeCb); + } + + // do search in chunks because the dispatcher used in the ios socket plugin can handle only 64 threads + checkAddressRange(addr, 64, checkAddressRangeCb); + } + + function checkInterfaceCb () { + if (ips.length === 0) { + onEnd(); + } else { + checkInterface(ips.pop(), checkInterfaceCb); + } + } + + checkInterface(ips.pop(), checkInterfaceCb); + + }.bind(this), function (error) { + this.showError(error); + }.bind(this)); + } + }); +}); diff --git a/assets/webconfig/js/app/data/ServerControl.js b/assets/webconfig/js/app/data/ServerControl.js new file mode 100644 index 0000000..6eb9f91 --- /dev/null +++ b/assets/webconfig/js/app/data/ServerControl.js @@ -0,0 +1,201 @@ +/*global define */ +define(['lib/stapes', 'utils/Tools'], function (Stapes, tools) { + 'use strict'; + + return Stapes.subclass(/** @lends ServerControl.prototype */{ + /** @type Socket */ + socket: null, + server: null, + connecting: false, + + /** + * @class ServerControl + * @classdesc Interface for the hyperion server control. All commands are sent directly to hyperion's server. + * @constructs + * @param {object} server - Hyperion server parameter + * @param {string} server.address - Server address + * @param {number} server.port - Hyperion server port + * @param {function} Socket - constructor of the socket to use for communication + * + * @fires connected + * @fires error + * @fires serverInfo + * @fires cmdSent + */ + constructor: function (server, Socket) { + this.server = server; + this.socket = new Socket(); + this.connecting = false; + }, + + /** + * Try to connect to the server + */ + connect: function () { + if (!this.server) { + this.emit('error', 'Missing server info'); + } else { + this.connecting = true; + this.socket.create(function () { + this.socket.connect(this.server, function () { + this.emit('connected'); + this.connecting = false; + }.bind(this), function (error) { + this.socket.close(); + this.emit('error', error); + this.connecting = false; + }.bind(this)); + }.bind(this)); + } + }, + + /** + * Disconnect from the server + */ + disconnect: function () { + this.socket.close(); + }, + + /** + * Sends the color command to the server + * @param {object} color - Color to set + * @param {number} color.r - Red value + * @param {number} color.g - Green value + * @param {number} color.b - Blue value + * @param {number} duration - Duration in seconds + */ + setColor: function (color, duration) { + var intColor, cmd; + + intColor = [ + Math.floor(color.r), Math.floor(color.g), Math.floor(color.b) + ]; + cmd = { + command: 'color', + color: intColor, + priority: this.server.priority + }; + + if (duration) { + cmd.duration = duration * 1000; + } + + this.sendCommand(cmd); + }, + + clear: function () { + var cmd = { + command: 'clear', + priority: this.server.priority + }; + this.sendCommand(cmd); + }, + + clearall: function () { + var cmd = { + command: 'clearall' + }; + this.sendCommand(cmd); + }, + + /** + * Sends a command to rund specified effect + * @param {object} effect - Effect object + */ + runEffect: function (effect) { + var cmd; + + if (!effect) { + return; + } + + cmd = { + command: 'effect', + effect: { + name: effect.name, + args: effect.args + }, + priority: this.server.priority + }; + this.sendCommand(cmd); + }, + + /** + * Sends a command for color transformation + * @param {object} transform + */ + setTransform: function (transform) { + var cmd; + + if (!transform) { + return; + } + + cmd = { + 'command': 'transform', + 'transform': transform + }; + + this.sendCommand(cmd); + }, + + /** + * @private + * @param command + */ + sendCommand: function (command) { + var data; + + if (!command) { + return; + } + + if (typeof command === 'string') { + data = command; + } else { + data = JSON.stringify(command); + } + + this.socket.isConnected(function (connected) { + if (connected) { + this.socket.write(data + '\n', function () { + this.emit('cmdSent', command); + }.bind(this), function (error) { + this.emit('error', error); + }.bind(this)); + } else { + this.emit('error', 'No server connection'); + } + }.bind(this)); + }, + + /** + * Get the information about the hyperion server + */ + getServerInfo: function () { + var cmd = {command: 'serverinfo'}; + + this.socket.isConnected(function (connected) { + if (connected) { + this.socket.write(JSON.stringify(cmd) + '\n', function () { + this.socket.read(function (result) { + var dataobj, str = tools.ab2str(result); + dataobj = JSON.parse(str); + this.emit('serverInfo', dataobj.info); + }.bind(this), function (error) { + this.emit('error', error); + }.bind(this)); + }.bind(this), function (error) { + this.emit('error', error); + }.bind(this)); + } else { + this.emit('error', 'No server connection'); + } + }.bind(this)); + }, + + isConnecting: function () { + return this.connecting; + } + }); +}); diff --git a/assets/webconfig/js/app/main.js b/assets/webconfig/js/app/main.js new file mode 100644 index 0000000..dfa2796 --- /dev/null +++ b/assets/webconfig/js/app/main.js @@ -0,0 +1,150 @@ +/*global require, requirejs */ + +requirejs.config({ + baseUrl: 'js/app', + paths: { + 'lib': '../vendor' + }, + map: { + 'controllers/AppController': { + 'api/Socket': 'api/WebSocket' + } + } +}); + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerDownHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchstart', handler, false); + dom.addEventListener('mousedown', handler, false); +}; + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.removePointerDownHandler = function (dom, handler) { + 'use strict'; + dom.removeEventListener('touchstart', handler, false); + dom.removeEventListener('mousedown', handler, false); +}; + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerUpHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchend', handler, false); + dom.addEventListener('mouseup', handler, false); +}; + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.removePointerUpHandler = function (dom, handler) { + 'use strict'; + dom.removeEventListener('touchend', handler, false); + dom.removeEventListener('mouseup', handler, false); +}; + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerMoveHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchmove', handler, false); + dom.addEventListener('mousemove', handler, false); +}; + +/** + * @param {HTMLElement} dom + * @param {function} handler + */ +window.removePointerMoveHandler = function (dom, handler) { + 'use strict'; + dom.removeEventListener('touchmove', handler, false); + dom.removeEventListener('mousemove', handler, false); +}; + +/** + * + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addClickHandler = function (dom, handler) { + 'use strict'; + var toFire = false; + + dom.addEventListener('touchstart', function (event) { + if (event.touches.length > 1) { + return; + } + toFire = true; + }, false); + + dom.addEventListener('touchmove', function () { + toFire = false; + }, false); + + dom.addEventListener('touchend', function (event) { + var focused; + if (toFire) { + handler.apply(this, arguments); + + focused = document.querySelector(':focus'); + + if (focused && event.target !== focused) { + focused.blur(); + } + + if (event.target.tagName !== 'INPUT') { + event.preventDefault(); + } + } + }, false); + + dom.addEventListener('click', function () { + handler.apply(this, arguments); + }, false); +}; + +function checkInstallFirefoxOS() { + 'use strict'; + var manifest_url, installCheck; + + manifest_url = [location.protocol, '//', location.host, location.pathname.replace('index.html',''), 'manifest.webapp'].join(''); + installCheck = navigator.mozApps.checkInstalled(manifest_url); + + installCheck.onerror = function() { + alert('Error calling checkInstalled: ' + installCheck.error.name); + }; + + installCheck.onsuccess = function() { + var installLoc; + if(!installCheck.result) { + if (confirm('Do you want to install hyperion remote contorl on your device?')) { + installLoc = navigator.mozApps.install(manifest_url); + installLoc.onsuccess = function(data) { + }; + installLoc.onerror = function() { + alert(installLoc.error.name); + }; + } + } + }; +} + +require(['controllers/AppController'], function (AppController) { + 'use strict'; + var app = new AppController(); + app.init(); + if (navigator.mozApps && navigator.userAgent.indexOf('Mozilla/5.0 (Mobile;') !== -1) { + checkInstallFirefoxOS(); + } +}); diff --git a/assets/webconfig/js/app/main_chrome.js b/assets/webconfig/js/app/main_chrome.js new file mode 100644 index 0000000..786030f --- /dev/null +++ b/assets/webconfig/js/app/main_chrome.js @@ -0,0 +1,108 @@ +/*global require, requirejs */ + +requirejs.config({ + baseUrl: '../js/app', + paths: { + 'lib': '../vendor' + }, + map: { + 'controllers/AppController': { + 'api/Socket': 'api/ChromeTcpSocket', + 'api/Network': 'api/ChromeNetwork' + }, + 'models/Settings': { + 'api/LocalStorage': 'api/ChromeLocalStorage' + } + } +}); + +/** + * + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerDownHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchstart', function (event) { + handler.apply(this, arguments); + event.preventDefault(); + }, false); + + dom.addEventListener('mousedown', function () { + handler.apply(this, arguments); + }, false); +}; + +/** + * + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerUpHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchend', function (event) { + handler.apply(this, arguments); + event.preventDefault(); + }, false); + + dom.addEventListener('mouseup', function () { + handler.apply(this, arguments); + }, false); +}; + +/** + * + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addPointerMoveHandler = function (dom, handler) { + 'use strict'; + dom.addEventListener('touchmove', function (event) { + handler.apply(this, arguments); + event.preventDefault(); + }, false); + + dom.addEventListener('mousemove', function () { + handler.apply(this, arguments); + }, false); +}; + +/** + * + * @param {HTMLElement} dom + * @param {function} handler + */ +window.addClickHandler = function (dom, handler) { + 'use strict'; + var toFire = false; + + dom.addEventListener('touchstart', function (event) { + if (event.touches.length > 1) { + return; + } + toFire = true; + }, false); + + dom.addEventListener('touchmove', function () { + toFire = false; + }, false); + + dom.addEventListener('touchend', function (event) { + if (toFire) { + handler.apply(this, arguments); + if (event.target.tagName !== 'INPUT') { + event.preventDefault(); + } + } + }, false); + + dom.addEventListener('click', function () { + handler.apply(this, arguments); + }, false); +}; + +require(['controllers/AppController'], function (AppController) { + 'use strict'; + var app = new AppController(); + app.init(); +}); diff --git a/assets/webconfig/js/app/models/Settings.js b/assets/webconfig/js/app/models/Settings.js new file mode 100644 index 0000000..5e224f0 --- /dev/null +++ b/assets/webconfig/js/app/models/Settings.js @@ -0,0 +1,124 @@ +/*global define */ +define(['lib/stapes', 'api/LocalStorage'], function (Stapes, LocalStorage) { + 'use strict'; + + return Stapes.subclass(/** @lends Settings.prototype */{ + storage: null, servers: [], + + /** + * @class Settings + * @classdesc Local application settings + * @constructs + * @fires saved + * @fires loaded + * @fires error + * @fires serverAdded + * @fires serverChanged + * @fires serverRemoved + */ + constructor: function () { + this.storage = new LocalStorage(); + this.storage.on({ + error: function (message) { + this.emit('error', message); + }, got: function (settings) { + if (settings) { + this.servers = settings.servers || []; + } + this.emit('loaded'); + }, set: function () { + this.emit('saved'); + } + }, this); + }, + + /** + * Save current settings + */ + save: function () { + this.storage.set({ + servers: this.servers + }); + }, + + /** + * Loads persistent settings + */ + load: function () { + this.storage.get(); + }, + + /** + * Add a server definition + * @param {object} server - Server information + */ + addServer: function (server) { + if (this.indexOfServer(server) === -1) { + if (this.servers.length === 0) { + server.selected = true; + } + + this.servers.push(server); + this.save(); + this.emit('serverAdded', server); + } + }, + + /** + * Sets a server as a default server + * @param {number} index - Index of the server in the server list to set as default one + */ + setSelectedServer: function (index) { + var i; + for (i = 0; i < this.servers.length; i++) { + delete this.servers[i].selected; + } + this.servers[index].selected = true; + this.save(); + this.emit('serverChanged', this.servers[index]); + }, + + /** + * Remove a server from the list + * @param {number} index - Index of the server in the list to remove + */ + removeServer: function (index) { + this.servers.splice(index, 1); + this.save(); + this.emit('serverRemoved'); + }, + + /** + * Update server information + * @param {number} index - Index of the server to update + * @param {object} server - New server information + */ + updateServer: function (index, server) { + if (index >= 0 && index < this.servers.length) { + this.servers[index] = server; + this.save(); + this.emit('serverChanged', server); + } + }, + + /** + * Find the server in the list. + * @param {object} server - Server to search index for + * @returns {number} - Index of the server in the list. -1 if server not found + */ + indexOfServer: function (server) { + var i, tmp; + + for (i = 0; i < this.servers.length; i++) { + tmp = this.servers[i]; + + if (tmp.port === server.port && tmp.address === server.address) { + return i; + } + } + + return -1; + } + + }); +}); diff --git a/assets/webconfig/js/app/utils/Tools.js b/assets/webconfig/js/app/utils/Tools.js new file mode 100644 index 0000000..2e2194c --- /dev/null +++ b/assets/webconfig/js/app/utils/Tools.js @@ -0,0 +1,56 @@ +define([], function () { + 'use strict'; + + return { + /** + * Convert a string to ArrayBuffer + * @param {string} str String to convert + * @returns {ArrayBuffer} Result + */ + str2ab: function (str) { + var i, buf = new ArrayBuffer(str.length), bufView = new Uint8Array(buf); + for (i = 0; i < str.length; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; + }, + + /** + * Convert an array to ArrayBuffer + * @param array + * @returns {ArrayBuffer} Result + */ + a2ab: function (array) { + return new Uint8Array(array).buffer; + }, + + /** + * Convert ArrayBuffer to string + * @param {ArrayBuffer} buffer Buffer to convert + * @returns {string} + */ + ab2hexstr: function (buffer) { + var i, str = '', ua = new Uint8Array(buffer); + for (i = 0; i < ua.length; i++) { + str += this.b2hexstr(ua[i]); + } + return str; + }, + + /** + * Convert byte to hexstr. + * @param {number} byte Byte to convert + */ + b2hexstr: function (byte) { + return ('00' + byte.toString(16)).substr(-2); + }, + + /** + * @param {ArrayBuffer} buffer + * @returns {string} + */ + ab2str: function (buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); + } + }; +}); diff --git a/assets/webconfig/js/app/views/EffectsView.js b/assets/webconfig/js/app/views/EffectsView.js new file mode 100644 index 0000000..4f41d07 --- /dev/null +++ b/assets/webconfig/js/app/views/EffectsView.js @@ -0,0 +1,64 @@ +/** + * hyperion remote + * MIT License + */ + +define(['lib/stapes'], function (Stapes) { + 'use strict'; + + return Stapes.subclass(/** @lends EffectsView.prototype */{ + /** + * @class EffectsView + * @constructs + */ + constructor: function () { + this.bindEventHandlers(); + }, + + /** + * @private + */ + bindEventHandlers: function () { + window.addClickHandler(document.querySelector('#effects ul'), function (event) { + var selected = event.target.parentNode.querySelector('.selected'); + if (selected) { + selected.classList.remove('selected'); + } + event.target.classList.add('selected'); + this.emit('effectSelected', event.target.dataset.id); + }.bind(this)); + }, + + /** + * Clear the list + */ + clear: function () { + document.querySelector('#effects ul').innerHTML = ''; + document.querySelector('#effects .info').classList.add('hidden'); + }, + + /** + * Fill the list + * @param {object} effects - Object containing effect information + */ + fillList: function (effects) { + var dom, el, i; + + dom = document.createDocumentFragment(); + + for (i = 0; i < effects.length; i++) { + el = document.createElement('li'); + el.innerHTML = effects[i].name; + el.dataset.id = effects[i].id; + dom.appendChild(el); + } + + document.querySelector('#effects ul').appendChild(dom); + + if (effects.length === 0) { + document.querySelector('#effects .info').classList.remove('hidden'); + } + } + }); +}); + diff --git a/assets/webconfig/js/app/views/MainView.js b/assets/webconfig/js/app/views/MainView.js new file mode 100644 index 0000000..986af3f --- /dev/null +++ b/assets/webconfig/js/app/views/MainView.js @@ -0,0 +1,396 @@ +define([ + 'lib/stapes', + 'lib/tinycolor', + 'utils/Tools', + 'views/Slider' +], function (Stapes, Tinycolor, tools, Slider) { + 'use strict'; + var timer; + + function showMessageField (text, type) { + var dom, wrapper; + + dom = document.querySelector('.work .msg'); + + if (!dom) { + dom = document.createElement('div'); + dom.classList.add('msg'); + dom.classList.add(type); + wrapper = document.createElement('div'); + wrapper.classList.add('wrapper_msg'); + wrapper.classList.add('invisible'); + wrapper.appendChild(dom); + + document.querySelector('.work').appendChild(wrapper); + setTimeout(function () { + wrapper.classList.remove('invisible'); + }, 0); + } + + if (!dom.classList.contains(type)) { + dom.className = 'msg'; + dom.classList.add(type); + } + dom.innerHTML = text; + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(function () { + var error = document.querySelector('.work .wrapper_msg'); + if (error) { + error.parentNode.removeChild(error); + } + }, 1600); + } + + return Stapes.subclass(/** @lends MainView.prototype*/{ + pointer: null, + colorpicker: null, + slider: null, + sliderinput: null, + cpradius: 0, + cpcenter: 0, + drag: false, + color: null, + brightness: 1.0, + inputbox: null, + + /** + * @class MainView + * @construct + * @fires barClick + * @fires colorChange + */ + constructor: function () { + var ev; + this.pointer = document.querySelector('#colorpicker #pointer'); + this.colorpicker = document.querySelector('#colorpicker #colorwheelbg'); + this.slider = new Slider({ + element: document.getElementById('brightness'), + min: 0, + max: 1, + step: 0.02, + value: 1 + }); + this.inputbox = document.querySelector('#color input.value'); + + this.cpradius = this.colorpicker.offsetWidth / 2; + this.cpcenter = this.colorpicker.offsetLeft + this.cpradius; + + this.bindEventHandlers(); + + ev = document.createEvent('Event'); + ev.initEvent('resize', true, true); + window.dispatchEvent(ev); + }, + + /** + * @private + */ + bindEventHandlers: function () { + var cptouchrect; + + window.addEventListener('resize', function () { + var attrW, attrH, side, w = this.colorpicker.parentNode.clientWidth, h = this.colorpicker.parentNode.clientHeight; + + attrW = this.colorpicker.getAttribute('width'); + attrH = this.colorpicker.getAttribute('height'); + side = attrW === 'auto' ? attrH : attrW; + if (w > h) { + if (attrH !== side) { + this.colorpicker.setAttribute('height', side); + this.colorpicker.setAttribute('width', 'auto'); + } + } else if (attrW !== side) { + this.colorpicker.setAttribute('height', 'auto'); + this.colorpicker.setAttribute('width', side); + } + + this.cpradius = this.colorpicker.offsetWidth / 2; + this.cpcenter = this.colorpicker.offsetLeft + this.cpradius; + if (this.color) { + this.updatePointer(); + } + }.bind(this)); + + window.addClickHandler(document.querySelector('.footer'), function (event) { + this.emit('barClick', event.target.parentNode.dataset.area); + }.bind(this)); + + + this.slider.on('changeValue', function (value) { + this.brightness = value.value; + this.updateInput(); + this.fireColorEvent(); + }, this); + + this.inputbox.addEventListener('input', function (event) { + var bright, rgb = new Tinycolor(event.target.value).toRgb(); + + if (rgb.r === 0 && rgb.g === 0 && rgb.b === 0) { + this.brightness = 0; + this.color = new Tinycolor({ + r: 0xff, + g: 0xff, + b: 0xff + }); + } else { + bright = Math.max(rgb.r, rgb.g, rgb.b) / 256; + rgb.r = Math.round(rgb.r / bright); + rgb.g = Math.round(rgb.g / bright); + rgb.b = Math.round(rgb.b / bright); + this.brightness = bright; + this.color = new Tinycolor(rgb); + } + + this.fireColorEvent(); + this.updatePointer(); + this.updateSlider(); + }.bind(this), false); + + this.inputbox.addEventListener('keydown', function (event) { + switch (event.keyCode) { + case 8: + case 9: + case 16: + case 37: + case 38: + case 39: + case 40: + case 46: + break; + default: + { + if (event.target.value.length >= 6 && (event.target.selectionEnd - event.target.selectionStart) === 0) { + event.preventDefault(); + event.stopPropagation(); + } else if (event.keyCode < 48 || event.keyCode > 71) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + }); + + cptouchrect = document.querySelector('#colorpicker .touchrect'); + window.addPointerDownHandler(cptouchrect, function (event) { + this.leaveInput(); + this.drag = true; + this.handleEvent(event); + }.bind(this)); + + window.addPointerMoveHandler(cptouchrect, function (event) { + if (this.drag) { + this.handleEvent(event); + event.preventDefault(); + } + }.bind(this)); + + window.addPointerUpHandler(cptouchrect, function () { + this.drag = false; + }.bind(this)); + + window.addClickHandler(document.querySelector('#clear_button'), function () { + this.emit('clear'); + }.bind(this)); + + window.addClickHandler(document.querySelector('#clearall_button'), function () { + this.emit('clearall'); + }.bind(this)); + }, + + /** + * @private + * @param event + */ + handleEvent: function (event) { + var point, x, y; + + if (event.touches) { + x = event.touches[0].clientX; + y = event.touches[0].clientY; + } else { + x = event.clientX; + y = event.clientY; + } + point = this.getCirclePoint(x, y); + this.color = this.getColorFromPoint(point); + + this.updatePointer(); + this.updateSlider(); + this.updateInput(); + + this.fireColorEvent(); + }, + + /** + * @private + * @param {number} x + * @param {number} y + * @returns {{x: number, y: number}} + */ + getCirclePoint: function (x, y) { + var p = { + x: x, + y: y + }, c = { + x: this.colorpicker.offsetLeft + this.cpradius, + y: this.colorpicker.offsetTop + this.cpradius + }, n; + + n = Math.sqrt(Math.pow((x - c.x), 2) + Math.pow((y - c.y), 2)); + + if (n > this.cpradius) { + p.x = (c.x) + this.cpradius * ((x - c.x) / n); + p.y = (c.y) + this.cpradius * ((y - c.y) / n); + } + + return p; + }, + + /** + * @private + * @param {{x: number, y: number}} p + * @returns {Tinycolor} + */ + getColorFromPoint: function (p) { + var h, t, s, x, y; + x = p.x - this.colorpicker.offsetLeft - this.cpradius; + y = this.cpradius - p.y + this.colorpicker.offsetTop; + t = Math.atan2(y, x); + h = (t * (180 / Math.PI) + 360) % 360; + s = Math.min(Math.sqrt(x * x + y * y) / this.cpradius, 1); + + return new Tinycolor({ + h: h, + s: s, + v: 1 + }); + }, + + /** + * @private + * @param color + * @returns {{x: number, y: number}} + */ + getPointFromColor: function (color) { + var t, x, y, p = {}; + + t = color.h * (Math.PI / 180); + y = Math.sin(t) * this.cpradius * color.s; + x = Math.cos(t) * this.cpradius * color.s; + + p.x = Math.round(x + this.colorpicker.offsetLeft + this.cpradius); + p.y = Math.round(this.cpradius - y + this.colorpicker.offsetTop); + + return p; + }, + + /** + * @private + */ + fireColorEvent: function () { + var rgb = this.color.toRgb(); + rgb.r = Math.round(rgb.r * this.brightness); + rgb.g = Math.round(rgb.g * this.brightness); + rgb.b = Math.round(rgb.b * this.brightness); + this.emit('colorChange', rgb); + }, + + /** + * + * @param rgb + */ + setColor: function (rgb) { + var bright; + + if (rgb.r === 0 && rgb.g === 0 && rgb.b === 0) { + this.brightness = 0; + this.color = new Tinycolor({ + r: 0xff, + g: 0xff, + b: 0xff + }); + } else { + bright = Math.max(rgb.r, rgb.g, rgb.b) / 256; + rgb.r = Math.round(rgb.r / bright); + rgb.g = Math.round(rgb.g / bright); + rgb.b = Math.round(rgb.b / bright); + this.brightness = bright; + this.color = new Tinycolor(rgb); + } + + this.updatePointer(); + this.updateSlider(); + this.updateInput(); + }, + + /** + * @private + */ + updateSlider: function () { + this.slider.setValue(this.brightness); + this.slider.dom.style.backgroundImage = '-webkit-linear-gradient(left, #000000 0%, ' + this.color.toHexString() + ' 100%)'; + }, + + /** + * @private + */ + updatePointer: function () { + var point = this.getPointFromColor(this.color.toHsv()); + + this.pointer.style.left = (point.x - this.pointer.offsetWidth / 2) + 'px'; + this.pointer.style.top = (point.y - this.pointer.offsetHeight / 2) + 'px'; + this.pointer.style.backgroundColor = this.color.toHexString(); + }, + + /** + * @private + */ + updateInput: function () { + var rgb = this.color.toRgb(); + rgb.r = Math.round(rgb.r * this.brightness); + rgb.g = Math.round(rgb.g * this.brightness); + rgb.b = Math.round(rgb.b * this.brightness); + + this.inputbox.value = tools.b2hexstr(rgb.r) + tools.b2hexstr(rgb.g) + tools.b2hexstr(rgb.b); + }, + + /** + * Scroll to the specific tab content + * @param {string} id - Id of the tab to scroll to + */ + scrollToArea: function (id) { + var area, index; + document.querySelector('.footer .selected').classList.remove('selected'); + document.querySelector('.footer .button[data-area =' + id + ']').classList.add('selected'); + + area = document.getElementById(id); + index = area.offsetLeft / area.clientWidth; + area.parentNode.style.left = (-index * 100) + '%'; + }, + + /** + * Shows a status message + * @param {string} message - Text to show + */ + showStatus: function (message) { + showMessageField(message, 'status'); + }, + + /** + * Shows the error text + * @param {string} error - Error message + */ + showError: function (error) { + showMessageField(error, 'error'); + }, + + /** + * @private + */ + leaveInput: function () { + this.inputbox.blur(); + } + }); +}); diff --git a/assets/webconfig/js/app/views/ServerList.js b/assets/webconfig/js/app/views/ServerList.js new file mode 100644 index 0000000..fb36927 --- /dev/null +++ b/assets/webconfig/js/app/views/ServerList.js @@ -0,0 +1,199 @@ +define(['lib/stapes'], function (Stapes) { + 'use strict'; + + /** + * + * @param params + * @returns {HTMLElement} + */ + function buildDom (params) { + var dom, label, ul, li, i, header; + + dom = document.createElement('div'); + dom.classList.add('grouplist'); + dom.id = params.id; + + header = document.createElement('div'); + header.classList.add('header'); + dom.appendChild(header); + label = document.createElement('div'); + label.innerHTML = params.label; + label.classList.add('title'); + header.appendChild(label); + label = document.createElement('div'); + label.innerHTML = ''; + label.classList.add('callout'); + header.appendChild(label); + + ul = document.createElement('ul'); + dom.appendChild(ul); + if (params.list) { + for (i = 0; i < params.list.length; i++) { + li = createLine(params.list[i]); + ul.appendChild(li); + } + } + + return dom; + } + + function createLine (params) { + var dom, el, horiz, box, touch; + + dom = document.createDocumentFragment(); + + horiz = document.createElement('div'); + horiz.classList.add('horizontal'); + dom.appendChild(horiz); + + touch = document.createElement('div'); + touch.classList.add('horizontal'); + horiz.appendChild(touch); + + el = document.createElement('div'); + el.classList.add('checkbox'); + touch.appendChild(el); + + box = document.createElement('div'); + box.classList.add('titlebox'); + touch.appendChild(box); + + el = document.createElement('label'); + el.classList.add('title'); + el.innerHTML = params.title || ''; + box.appendChild(el); + + el = document.createElement('label'); + el.classList.add('subtitle'); + el.innerHTML = params.subtitle; + box.appendChild(el); + + el = document.createElement('div'); + el.classList.add('touchrect'); + touch.appendChild(el); + + el = document.createElement('div'); + el.classList.add('edit_icon'); + el.innerHTML = ''; + horiz.appendChild(el); + + el = document.createElement('div'); + el.classList.add('delete_icon'); + el.innerHTML = ''; + horiz.appendChild(el); + + return dom; + } + + return Stapes.subclass(/** @lends ServerList.prototype */{ + /** + * @private + * @type {HTMLElement} + */ + dom: null, + + /** + * @class ServerList + * @constructs + * @param {object} params - List parameter + * @param {string} params.id - List id + * @param {string} params.label - List title + * @param {{title: string, subtitle: string, c}[]} [params.list] - List elements + * + * @fires add + * @fires remove + * @fires select + * @fires edit + */ + constructor: function (params) { + this.dom = buildDom(params || {}); + this.bindEventHandlers(); + }, + + /** + * @private + */ + bindEventHandlers: function () { + window.addClickHandler(this.dom.querySelector('.callout'), function () { + this.emit('add'); + }.bind(this)); + + window.addClickHandler(this.dom.querySelector('ul'), function (event) { + if (event.target.classList.contains('delete_icon')) { + this.emit('remove', event.target.parentNode.parentNode.id); + } else if (event.target.classList.contains('edit_icon')) { + this.emit('edit', event.target.parentNode.parentNode.id); + } else if (event.target.classList.contains('touchrect')) { + this.emit('select', event.target.parentNode.parentNode.parentNode.id); + } + }.bind(this)); + }, + + /** + * Returns the DOM of the list + * @returns {HTMLElement} + */ + getDom: function () { + return this.dom; + }, + + /** + * Append a line + * @param id + * @param selected + * @param element + */ + append: function (id, selected, element) { + var li = document.createElement('li'); + li.id = id; + if (selected) { + li.classList.add('selected'); + } + li.appendChild(element); + this.dom.querySelector('ul').appendChild(li); + }, + + /** + * Replace a line + * @param index + * @param element + */ + replace: function (index, element) { + var child = this.dom.querySelector('ul li:nth-child(' + (index + 1) + ')'), li; + if (child) { + li = document.createElement('li'); + li.appendChild(element); + this.dom.querySelector('ul').replaceChild(li, child); + } + }, + + /** + * Add a line + * @param lineParam + */ + addLine: function (lineParam) { + var line = createLine(lineParam); + this.append(lineParam.id, lineParam.selected, line); + }, + + /** + * Clear the list + */ + clear: function () { + this.dom.querySelector('ul').innerHTML = ''; + }, + + /** + * Hide or show the Add button in the header + * @param {boolean} show - True to show, false to hide + */ + showAddButton: function (show) { + if (show) { + this.dom.querySelector('.callout').classList.remove('invisible'); + } else { + this.dom.querySelector('.callout').classList.add('invisible'); + } + } + }); +}); + diff --git a/assets/webconfig/js/app/views/SettingsView.js b/assets/webconfig/js/app/views/SettingsView.js new file mode 100644 index 0000000..38c24f8 --- /dev/null +++ b/assets/webconfig/js/app/views/SettingsView.js @@ -0,0 +1,259 @@ +define(['lib/stapes', 'views/ServerList'], function (Stapes, ServerList) { + 'use strict'; + + function createLabelInputLine (params) { + var dom, el; + + dom = document.createElement('div'); + dom.classList.add('inputline'); + dom.id = params.id; + + el = document.createElement('label'); + el.innerHTML = params.label; + dom.appendChild(el); + + el = document.createElement('input'); + if (typeof params.value === 'number') { + el.type = 'number'; + } + el.value = params.value || ''; + el.autocomplete='off'; + el.autocorrect='off'; + el.autocapitalize='off'; + dom.appendChild(el); + + return dom; + } + + function createButtonLine (params) { + var dom, el; + + dom = document.createElement('div'); + dom.classList.add('inputline'); + + el = document.createElement('button'); + el.innerHTML = params.label; + el.classList.add('OK'); + dom.appendChild(el); + + el = document.createElement('button'); + el.innerHTML = 'Cancel'; + el.classList.add('CANCEL'); + dom.appendChild(el); + + return dom; + } + + function createDetectLine () { + var dom, el; + + dom = document.createElement('div'); + dom.classList.add('line'); + + el = document.createElement('button'); + el.id = 'detect_button'; + el.innerHTML = 'Detect'; + dom.appendChild(el); + + el = document.createElement('div'); + el.classList.add('spinner'); + el.classList.add('hidden'); + dom.appendChild(el); + + return dom; + } + + return Stapes.subclass(/** @lends SettingsView.prototype */{ + dom: null, + serverList: null, + + /** + * @class SettingsView + * @classdesc View for the settings + * @constructs + */ + constructor: function () { + var list = [], el; + + this.dom = document.querySelector('#settings'); + + this.serverList = new ServerList({ + id: 'serverList', + label: 'Server', + list: list + }); + + this.serverList.on({ + add: function () { + var line, box; + + this.enableDetectButton(false); + this.lockList(true); + + box = document.createDocumentFragment(); + + line = createLabelInputLine({id: 'name', label: 'Name:'}); + box.appendChild(line); + line = createLabelInputLine({id: 'address', label: 'Address:'}); + box.appendChild(line); + line = createLabelInputLine({id: 'port', label: 'Port:', value: 19444}); + box.appendChild(line); + line = createLabelInputLine({id: 'priority', label: 'Priority:', value: 50}); + box.appendChild(line); + line = createLabelInputLine({id: 'duration', label: 'Duration (sec):', value: 0}); + box.appendChild(line); + + line = createButtonLine({label: 'Add'}); + + window.addClickHandler(line.firstChild, function (event) { + var server = {}, i, inputs = event.target.parentNode.parentNode.querySelectorAll('input'); + + for (i = 0; i < inputs.length; i++) { + server[inputs[i].parentNode.id] = inputs[i].value; + } + + server.port = parseInt(server.port); + server.priority = parseInt(server.priority); + server.duration = parseInt(server.duration); + + this.emit('serverAdded', server); + }.bind(this)); + + window.addClickHandler(line.lastChild, function () { + this.emit('serverAddCanceled'); + }.bind(this)); + box.appendChild(line); + this.serverList.append(null, false, box); + }, + select: function (id) { + if (!this.dom.classList.contains('locked')) { + this.emit('serverSelected', parseInt(id.replace('server_', ''))); + } + }, + remove: function (id) { + this.emit('serverRemoved', parseInt(id.replace('server_', ''))); + }, + edit: function (id) { + this.emit('editServer', parseInt(id.replace('server_', ''))); + } + }, this); + + this.dom.appendChild(this.serverList.getDom()); + + el = createDetectLine(); + + window.addClickHandler(el.querySelector('button'), function () { + this.emit('detect'); + }.bind(this)); + + this.dom.appendChild(el); + }, + + /** + * Fills the list of known servers + * @param {Array} servers + */ + fillServerList: function (servers) { + var i, server, params; + this.serverList.clear(); + for (i = 0; i < servers.length; i++) { + server = servers[i]; + params = {id: 'server_' + i, title: server.name, subtitle: server.address + ':' + server.port}; + if (server.selected) { + params.selected = true; + } + this.serverList.addLine(params); + } + this.serverList.showAddButton(true); + }, + + /** + * Shows or hides the spinner as progress indicator + * @param {boolean} show True to show, false to hide + */ + showWaiting: function (show) { + if (show) { + this.dom.querySelector('.spinner').classList.remove('hidden'); + } else { + this.dom.querySelector('.spinner').classList.add('hidden'); + } + }, + + /** + * Enables or disables the detect button + * @param {Boolean} enabled True to enable, false to disable + */ + enableDetectButton: function (enabled) { + if (enabled) { + this.dom.querySelector('#detect_button').classList.remove('hidden'); + } else { + this.dom.querySelector('#detect_button').classList.add('hidden'); + } + }, + + /** + * Locks the list for editing/deleting + * @param {Boolean} lock True to lock, false to unlock + */ + lockList: function (lock) { + if (!lock) { + this.dom.classList.remove('locked'); + this.serverList.showAddButton(true); + } else { + this.dom.classList.add('locked'); + this.serverList.showAddButton(false); + } + }, + + editServer: function (serverInfo) { + var line, box; + + this.lockList(true); + this.enableDetectButton(false); + + box = document.createDocumentFragment(); + + line = createLabelInputLine({id: 'name', label: 'Name:', value: serverInfo.server.name}); + box.appendChild(line); + line = createLabelInputLine({id: 'address', label: 'Address:', value: serverInfo.server.address}); + box.appendChild(line); + line = createLabelInputLine({id: 'port', label: 'Port:', value: serverInfo.server.port}); + box.appendChild(line); + line = createLabelInputLine({id: 'priority', label: 'Priority:', value: serverInfo.server.priority}); + box.appendChild(line); + line = createLabelInputLine({id: 'duration', label: 'Duration (sec):', value: serverInfo.server.duration}); + box.appendChild(line); + + line = createButtonLine({label: 'Done'}); + + window.addClickHandler(line.querySelector('button.OK'), function (event) { + var server = {}, i, inputs = event.target.parentNode.parentNode.querySelectorAll('input'); + + for (i = 0; i < inputs.length; i++) { + server[inputs[i].parentNode.id] = inputs[i].value; + } + + server.port = parseInt(server.port); + server.priority = parseInt(server.priority); + server.duration = parseInt(server.duration); + if (serverInfo.server.selected) { + server.selected = true; + } + + this.emit('serverChanged', {index: serverInfo.index, server: server}); + }.bind(this)); + + window.addClickHandler(line.querySelector('button.CANCEL'), function () { + this.emit('serverEditCanceled'); + }.bind(this)); + box.appendChild(line); + + window.addClickHandler(box.querySelector('input'), function (event) { + event.stopPropagation(); + }); + + this.serverList.replace(serverInfo.index, box); + } + }); +}); + diff --git a/assets/webconfig/js/app/views/Slider.js b/assets/webconfig/js/app/views/Slider.js new file mode 100644 index 0000000..84ab6af --- /dev/null +++ b/assets/webconfig/js/app/views/Slider.js @@ -0,0 +1,134 @@ +define(['lib/stapes'], function (Stapes) { + 'use strict'; + + function syncView (slider) { + var left = (slider.value - slider.min) * 100 / (slider.max - slider.min); + slider.dom.lastElementChild.style.left = left + '%'; + } + + function handleEvent(event, slider) { + var x, left, ratio, value, steppedValue; + if (event.touches) { + x = event.touches[0].clientX; + } else { + x = event.clientX; + } + + left = x - slider.dom.getBoundingClientRect().left; + ratio = left / slider.dom.offsetWidth; + value = (slider.max - slider.min) * ratio; + + steppedValue = (value - slider.min) % slider.step; + if (steppedValue <= slider.step / 2) { + value = value - steppedValue; + } else { + value = value + (slider.step - steppedValue); + } + + value = Math.max(value, slider.min); + value = Math.min(value, slider.max); + slider.value = value; + slider.emit('changeValue', { + 'value': value, + 'target': slider.dom + }); + } + + return Stapes.subclass(/** @lends Slider.prototype */{ + /** + * @private + * @type {Element} + */ + dom: null, + + /** + * @private + * @type {Number} + */ + min: 0, + + /** + * @private + * @type {Number} + */ + max: 100, + + /** + * @private + * @type {Number} + */ + value: 0, + + /** + * @private + * @type {Number} + */ + step: 1, + + /** + * @class Slider + * @constructs + * @fires change + */ + constructor: function (params) { + this.dom = params.element; + this.setValue(params.value); + this.setMin(params.min); + this.setMax(params.max); + this.setStep(params.step); + + this.bindEventHandlers(); + }, + + /** + * @private + */ + bindEventHandlers: function () { + var that = this; + + function pointerMoveEventHandler(event) { + if (that.drag) { + handleEvent(event, that); + syncView(that); + event.preventDefault(); + } + } + + function pointerUpEventHandler() { + that.drag = false; + syncView(that); + window.removePointerMoveHandler(document, pointerMoveEventHandler); + window.removePointerUpHandler(document, pointerUpEventHandler); + } + + window.addPointerDownHandler(this.dom, function (event) { + this.drag = true; + handleEvent(event, this); + syncView(this); + window.addPointerMoveHandler(document, pointerMoveEventHandler); + window.addPointerUpHandler(document, pointerUpEventHandler); + }.bind(this)); + }, + + setValue: function(value) { + this.value = value || 0; + syncView(this); + }, + + setMin: function(value) { + this.min = value || 0; + syncView(this); + }, + + setMax: function(value) { + this.max = value || 100; + syncView(this); + }, + + setStep: function(value) { + this.step = value || 1; + syncView(this); + } + }); +}); + diff --git a/assets/webconfig/js/app/views/TransformView.js b/assets/webconfig/js/app/views/TransformView.js new file mode 100644 index 0000000..6fe968a --- /dev/null +++ b/assets/webconfig/js/app/views/TransformView.js @@ -0,0 +1,375 @@ +/** + * hyperion remote + * MIT License + */ + +define([ + 'lib/stapes', + 'views/Slider' +], function (Stapes, Slider) { + 'use strict'; + + function onHeaderClick (event) { + var list = event.target.parentNode.parentNode.querySelector('ul'); + + if (list.clientHeight === 0) { + list.style.maxHeight = list.scrollHeight + 'px'; + event.target.parentNode.parentNode.setAttribute('collapsed', 'false'); + } else { + list.style.maxHeight = 0; + event.target.parentNode.parentNode.setAttribute('collapsed', 'true'); + } + } + + function createLine (id, type, icon, caption, value, min, max) { + var dom, el, el2, label, wrapper; + + dom = document.createElement('li'); + dom.className = type; + + label = document.createElement('label'); + label.innerHTML = caption; + dom.appendChild(label); + + wrapper = document.createElement('div'); + wrapper.classList.add('wrapper'); + wrapper.id = id; + + el = document.createElement('div'); + el.classList.add('icon'); + el.innerHTML = icon; + wrapper.appendChild(el); + + el = document.createElement('div'); + el.classList.add('slider'); + el2 = document.createElement('div'); + el2.classList.add('track'); + el.appendChild(el2); + el2 = document.createElement('div'); + el2.classList.add('thumb'); + el.appendChild(el2); + el.dataset.min = min; + el.dataset.max = max; + el.dataset.value = value; + el.dataset.step = 0.01; + wrapper.appendChild(el); + + el = document.createElement('input'); + el.classList.add('value'); + el.type = 'number'; + el.min = min; + el.max = max; + el.step = 0.01; + el.value = parseFloat(Math.round(value * 100) / 100).toFixed(2); + wrapper.appendChild(el); + + dom.appendChild(wrapper); + + return dom; + } + + function createGroup (groupInfo) { + var group, node, subnode, i, member; + group = document.createElement('div'); + group.classList.add('group'); + if (groupInfo.collapsed) { + group.setAttribute('collapsed', 'true'); + } + group.id = groupInfo.id; + + node = document.createElement('div'); + node.classList.add('header'); + group.appendChild(node); + subnode = document.createElement('label'); + subnode.innerHTML = groupInfo.title; + node.appendChild(subnode); + subnode = document.createElement('label'); + subnode.innerHTML = groupInfo.subtitle; + node.appendChild(subnode); + + node = document.createElement('ul'); + group.appendChild(node); + for (i = 0; i < groupInfo.members.length; i++) { + member = groupInfo.members[i]; + subnode = createLine(member.id, member.type, member.icon, member.label, member.value, member.min, + member.max); + node.appendChild(subnode); + } + + return group; + } + + return Stapes.subclass(/** @lends TransformView.prototype */{ + sliders: {}, + + /** + * @class TransformView + * @constructs + */ + constructor: function () { + }, + + /** + * Clear the list + */ + clear: function () { + document.querySelector('#transform .values').innerHTML = ''; + }, + + /** + * @private + * @param change + */ + onSliderChange: function (event) { + var data = {}, idparts, value; + + idparts = event.target.parentNode.id.split('_'); + value = parseFloat(Math.round(parseFloat(event.value) * 100) / 100); + + event.target.parentNode.querySelector('.value').value = value.toFixed(2); + + data[idparts[1]] = value; + this.emit(idparts[0], data); + }, + + /** + * @private + * @param change + */ + onValueChange: function (event) { + var data = {}, idparts, value; + + idparts = event.target.parentNode.id.split('_'); + value = parseFloat(Math.round(parseFloat(event.target.value) * 100) / 100); + + if (parseFloat(event.target.value) < parseFloat(event.target.min)) { + event.target.value = event.target.min; + } else if (parseFloat(event.target.value) > parseFloat(event.target.max)) { + event.target.value = event.target.max; + } + this.sliders[event.target.parentNode.id].setValue(value); + + data[idparts[1]] = value; + this.emit(idparts[0], data); + }, + + /** + * fill the list + * @param {object} transform - Object containing transform information + */ + fillList: function (transform) { + var dom, group, els, i, slider; + + if (!transform) { + document.querySelector('#transform .info').classList.remove('hidden'); + return; + } + + dom = document.createDocumentFragment(); + + group = createGroup({ + collapsed: true, + id: 'HSV', + title: 'HSV', + subtitle: 'HSV color corrections', + members: [ + { + id: 'hsv_saturationGain', + type: 'saturation', + icon: '', + label: 'Saturation gain', + value: transform.saturationGain, + min: 0, + max: 5 + }, + { + id: 'hsv_valueGain', + type: 'value', + icon: '', + label: 'Value gain', + value: transform.valueGain, + min: 0, + max: 5 + } + ] + }); + dom.appendChild(group); + + group = createGroup({ + collapsed: true, + id: 'Gamma', + title: 'Gamma', + subtitle: 'Gamma correction', + members: [ + { + id: 'gamma_r', + type: 'red', + icon: '', + label: 'Red', + value: transform.gamma[0], + min: 0, + max: 5 + }, + { + id: 'gamma_g', + type: 'green', + icon: '', + label: 'Green', + value: transform.gamma[1], + min: 0, + max: 5 + }, + { + id: 'gamma_b', + type: 'blue', + icon: '', + label: 'Blue', + value: transform.gamma[2], + min: 0, + max: 5 + } + ] + }); + dom.appendChild(group); + + group = createGroup({ + collapsed: true, + id: 'Whitelevel', + title: 'Whitelevel', + subtitle: 'Value when RGB channel is fully on', + members: [ + { + id: 'whitelevel_r', + type: 'red', + icon: '', + label: 'Red', + value: transform.whitelevel[0], + min: 0, + max: 1 + }, + { + id: 'whitelevel_g', + type: 'green', + icon: '', + label: 'Green', + value: transform.whitelevel[1], + min: 0, + max: 1 + }, + { + id: 'whitelevel_b', + type: 'blue', + icon: '', + label: 'Blue', + value: transform.whitelevel[2], + min: 0, + max: 1 + } + ] + }); + dom.appendChild(group); + + group = createGroup({ + collapsed: true, + id: 'Blacklevel', + title: 'Blacklevel', + subtitle: 'Value when RGB channel is fully off', + members: [ + { + id: 'blacklevel_r', + type: 'red', + icon: '', + label: 'Red', + value: transform.blacklevel[0], + min: 0, + max: 1 + }, + { + id: 'blacklevel_g', + type: 'green', + icon: '', + label: 'Green', + value: transform.blacklevel[1], + min: 0, + max: 1 + }, + { + id: 'blacklevel_b', + type: 'blue', + icon: '', + label: 'Blue', + value: transform.blacklevel[2], + min: 0, + max: 1 + } + ] + }); + dom.appendChild(group); + + group = createGroup({ + collapsed: true, + id: 'Threshold', + title: 'Threshold', + subtitle: 'Threshold for a channel', + members: [ + { + id: 'threshold_r', + type: 'red', + icon: '', + label: 'Red', + value: transform.threshold[0], + min: 0, + max: 1 + }, + { + id: 'threshold_g', + type: 'green', + icon: '', + label: 'Green', + value: transform.threshold[1], + min: 0, + max: 1 + }, + { + id: 'threshold_b', + type: 'blue', + icon: '', + label: 'Blue', + value: transform.threshold[2], + min: 0, + max: 1 + } + ] + }); + dom.appendChild(group); + + els = dom.querySelectorAll('.slider'); + for (i = 0; i < els.length; i++) { + slider = new Slider({ + element: els[i], + min: els[i].dataset.min, + max: els[i].dataset.max, + step: els[i].dataset.step, + value: els[i].dataset.value + }); + slider.on('changeValue', this.onSliderChange, this); + this.sliders[els[i].parentNode.id] = slider; + } + + els = dom.querySelectorAll('input'); + for (i = 0; i < els.length; i++) { + els[i].addEventListener('input', this.onValueChange.bind(this), false); + } + + els = dom.querySelectorAll('.header'); + for (i = 0; i < els.length; i++) { + window.addClickHandler(els[i], onHeaderClick); + } + + document.querySelector('#transform .info').classList.add('hidden'); + document.querySelector('#transform .values').appendChild(dom); + } + + }); +}); + diff --git a/assets/webconfig/js/background.js b/assets/webconfig/js/background.js new file mode 100644 index 0000000..0f68ebb --- /dev/null +++ b/assets/webconfig/js/background.js @@ -0,0 +1,17 @@ +/*global chrome */ +chrome.app.runtime.onLaunched.addListener(function () { + 'use strict'; + chrome.app.window.create('index.html', { + 'id': 'fakeIdForSingleton', + 'innerBounds': { + 'width': 320, + 'height': 480, + 'minWidth': 320, + 'minHeight': 480 + }, + resizable: false + }); +}); + + + diff --git a/assets/webconfig/js/vendor/require.js b/assets/webconfig/js/vendor/require.js new file mode 100644 index 0000000..2ce09b5 --- /dev/null +++ b/assets/webconfig/js/vendor/require.js @@ -0,0 +1,2054 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.1.9 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ +//Not using strict: uneven strict support in browsers, #392, and causes +//problems with requirejs.exec()/transpiler plugins that may not be strict. +/*jslint regexp: true, nomen: true, sloppy: true */ +/*global window, navigator, document, importScripts, setTimeout, opera */ + +var requirejs, require, define; +(function (global) { + var req, s, head, baseElement, dataMain, src, + interactiveScript, currentlyAddingScript, mainScript, subPath, + version = '2.1.9', + commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, + cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, + jsSuffixRegExp = /\.js$/, + currDirRegExp = /^\.\//, + op = Object.prototype, + ostring = op.toString, + hasOwn = op.hasOwnProperty, + ap = Array.prototype, + apsp = ap.splice, + isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), + isWebWorker = !isBrowser && typeof importScripts !== 'undefined', + //PS3 indicates loaded and complete, but need to wait for complete + //specifically. Sequence is 'loading', 'loaded', execution, + // then 'complete'. The UA check is unfortunate, but not sure how + //to feature test w/o causing perf issues. + readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? + /^complete$/ : /^(complete|loaded)$/, + defContextName = '_', + //Oh the tragedy, detecting opera. See the usage of isOpera for reason. + isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', + contexts = {}, + cfg = {}, + globalDefQueue = [], + useInteractive = false; + + function isFunction(it) { + return ostring.call(it) === '[object Function]'; + } + + function isArray(it) { + return ostring.call(it) === '[object Array]'; + } + + /** + * Helper function for iterating over an array. If the func returns + * a true value, it will break out of the loop. + */ + function each(ary, func) { + if (ary) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + /** + * Helper function for iterating over an array backwards. If the func + * returns a true value, it will break out of the loop. + */ + function eachReverse(ary, func) { + if (ary) { + var i; + for (i = ary.length - 1; i > -1; i -= 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + function getOwn(obj, prop) { + return hasProp(obj, prop) && obj[prop]; + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + function eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name. + */ + function mixin(target, source, force, deepStringMixin) { + if (source) { + eachProp(source, function (value, prop) { + if (force || !hasProp(target, prop)) { + if (deepStringMixin && typeof value !== 'string') { + if (!target[prop]) { + target[prop] = {}; + } + mixin(target[prop], value, force, deepStringMixin); + } else { + target[prop] = value; + } + } + }); + } + return target; + } + + //Similar to Function.prototype.bind, but the 'this' object is specified + //first, since it is easier to read/figure out what 'this' will be. + function bind(obj, fn) { + return function () { + return fn.apply(obj, arguments); + }; + } + + function scripts() { + return document.getElementsByTagName('script'); + } + + function defaultOnError(err) { + throw err; + } + + //Allow getting a global that expressed in + //dot notation, like 'a.b.c'. + function getGlobal(value) { + if (!value) { + return value; + } + var g = global; + each(value.split('.'), function (part) { + g = g[part]; + }); + return g; + } + + /** + * Constructs an error with a pointer to an URL with more information. + * @param {String} id the error ID that maps to an ID on a web page. + * @param {String} message human readable error. + * @param {Error} [err] the original error, if there is one. + * + * @returns {Error} + */ + function makeError(id, msg, err, requireModules) { + var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); + e.requireType = id; + e.requireModules = requireModules; + if (err) { + e.originalError = err; + } + return e; + } + + if (typeof define !== 'undefined') { + //If a define is already in play via another AMD loader, + //do not overwrite. + return; + } + + if (typeof requirejs !== 'undefined') { + if (isFunction(requirejs)) { + //Do not overwrite and existing requirejs instance. + return; + } + cfg = requirejs; + requirejs = undefined; + } + + //Allow for a require config object + if (typeof require !== 'undefined' && !isFunction(require)) { + //assume it is a config object. + cfg = require; + require = undefined; + } + + function newContext(contextName) { + var inCheckLoaded, Module, context, handlers, + checkLoadedTimeoutId, + config = { + //Defaults. Do not set a default for map + //config to speed up normalize(), which + //will run faster if there is no default. + waitSeconds: 7, + baseUrl: './', + paths: {}, + pkgs: {}, + shim: {}, + config: {} + }, + registry = {}, + //registry of just enabled modules, to speed + //cycle breaking code when lots of modules + //are registered, but not activated. + enabledRegistry = {}, + undefEvents = {}, + defQueue = [], + defined = {}, + urlFetched = {}, + requireCounter = 1, + unnormalizedCounter = 1; + + /** + * Trims the . and .. from an array of path segments. + * It will keep a leading path segment if a .. will become + * the first path segment, to help with module name lookups, + * which act like paths, but can be remapped. But the end result, + * all paths that use this function should look normalized. + * NOTE: this method MODIFIES the input array. + * @param {Array} ary the array of path segments. + */ + function trimDots(ary) { + var i, part; + for (i = 0; ary[i]; i += 1) { + part = ary[i]; + if (part === '.') { + ary.splice(i, 1); + i -= 1; + } else if (part === '..') { + if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + ary.splice(i - 1, 2); + i -= 2; + } + } + } + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @param {Boolean} applyMap apply the map config to the value. Should + * only be done if this normalization is for a dependency ID. + * @returns {String} normalized name + */ + function normalize(name, baseName, applyMap) { + var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment, + foundMap, foundI, foundStarMap, starI, + baseParts = baseName && baseName.split('/'), + normalizedBaseParts = baseParts, + map = config.map, + starMap = map && map['*']; + + //Adjust any relative paths. + if (name && name.charAt(0) === '.') { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + if (getOwn(config.pkgs, baseName)) { + //If the baseName is a package name, then just treat it as one + //name to concat the name with. + normalizedBaseParts = baseParts = [baseName]; + } else { + //Convert baseName to array, and lop off the last part, + //so that . matches that 'directory' and not name of the baseName's + //module. For instance, baseName of 'one/two/three', maps to + //'one/two/three.js', but we want the directory, 'one/two' for + //this normalization. + normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); + } + + name = normalizedBaseParts.concat(name.split('/')); + trimDots(name); + + //Some use of packages may use a . path to reference the + //'main' module name, so normalize for that. + pkgConfig = getOwn(config.pkgs, (pkgName = name[0])); + name = name.join('/'); + if (pkgConfig && name === pkgName + '/' + pkgConfig.main) { + name = pkgName; + } + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if (applyMap && map && (baseParts || starMap)) { + nameParts = name.split('/'); + + for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join('/'); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = getOwn(map, baseParts.slice(0, j).join('/')); + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = getOwn(mapValue, nameSegment); + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break; + } + } + } + } + + if (foundMap) { + break; + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { + foundStarMap = getOwn(starMap, nameSegment); + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + return name; + } + + function removeScript(name) { + if (isBrowser) { + each(scripts(), function (scriptNode) { + if (scriptNode.getAttribute('data-requiremodule') === name && + scriptNode.getAttribute('data-requirecontext') === context.contextName) { + scriptNode.parentNode.removeChild(scriptNode); + return true; + } + }); + } + } + + function hasPathFallback(id) { + var pathConfig = getOwn(config.paths, id); + if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { + //Pop off the first array value, since it failed, and + //retry + pathConfig.shift(); + context.require.undef(id); + context.require([id]); + return true; + } + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Creates a module mapping that includes plugin prefix, module + * name, and path. If parentModuleMap is provided it will + * also normalize the name via require.normalize() + * + * @param {String} name the module name + * @param {String} [parentModuleMap] parent module map + * for the module name, used to resolve relative names. + * @param {Boolean} isNormalized: is the ID already normalized. + * This is true if this call is done for a define() module ID. + * @param {Boolean} applyMap: apply the map config to the ID. + * Should only be true if this map is for a dependency. + * + * @returns {Object} + */ + function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { + var url, pluginModule, suffix, nameParts, + prefix = null, + parentName = parentModuleMap ? parentModuleMap.name : null, + originalName = name, + isDefine = true, + normalizedName = ''; + + //If no name, then it means it is a require call, generate an + //internal name. + if (!name) { + isDefine = false; + name = '_@r' + (requireCounter += 1); + } + + nameParts = splitPrefix(name); + prefix = nameParts[0]; + name = nameParts[1]; + + if (prefix) { + prefix = normalize(prefix, parentName, applyMap); + pluginModule = getOwn(defined, prefix); + } + + //Account for relative paths if there is a base name. + if (name) { + if (prefix) { + if (pluginModule && pluginModule.normalize) { + //Plugin is loaded, use its normalize method. + normalizedName = pluginModule.normalize(name, function (name) { + return normalize(name, parentName, applyMap); + }); + } else { + normalizedName = normalize(name, parentName, applyMap); + } + } else { + //A regular module. + normalizedName = normalize(name, parentName, applyMap); + + //Normalized name may be a plugin ID due to map config + //application in normalize. The map config values must + //already be normalized, so do not need to redo that part. + nameParts = splitPrefix(normalizedName); + prefix = nameParts[0]; + normalizedName = nameParts[1]; + isNormalized = true; + + url = context.nameToUrl(normalizedName); + } + } + + //If the id is a plugin id that cannot be determined if it needs + //normalization, stamp it with a unique ID so two matching relative + //ids that may conflict can be separate. + suffix = prefix && !pluginModule && !isNormalized ? + '_unnormalized' + (unnormalizedCounter += 1) : + ''; + + return { + prefix: prefix, + name: normalizedName, + parentMap: parentModuleMap, + unnormalized: !!suffix, + url: url, + originalName: originalName, + isDefine: isDefine, + id: (prefix ? + prefix + '!' + normalizedName : + normalizedName) + suffix + }; + } + + function getModule(depMap) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (!mod) { + mod = registry[id] = new context.Module(depMap); + } + + return mod; + } + + function on(depMap, name, fn) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (hasProp(defined, id) && + (!mod || mod.defineEmitComplete)) { + if (name === 'defined') { + fn(defined[id]); + } + } else { + mod = getModule(depMap); + if (mod.error && name === 'error') { + fn(mod.error); + } else { + mod.on(name, fn); + } + } + } + + function onError(err, errback) { + var ids = err.requireModules, + notified = false; + + if (errback) { + errback(err); + } else { + each(ids, function (id) { + var mod = getOwn(registry, id); + if (mod) { + //Set error on module, so it skips timeout checks. + mod.error = err; + if (mod.events.error) { + notified = true; + mod.emit('error', err); + } + } + }); + + if (!notified) { + req.onError(err); + } + } + } + + /** + * Internal method to transfer globalQueue items to this context's + * defQueue. + */ + function takeGlobalQueue() { + //Push all the globalDefQueue items into the context's defQueue + if (globalDefQueue.length) { + //Array splice in the values since the context code has a + //local var ref to defQueue, so cannot just reassign the one + //on context. + apsp.apply(defQueue, + [defQueue.length - 1, 0].concat(globalDefQueue)); + globalDefQueue = []; + } + } + + handlers = { + 'require': function (mod) { + if (mod.require) { + return mod.require; + } else { + return (mod.require = context.makeRequire(mod.map)); + } + }, + 'exports': function (mod) { + mod.usingExports = true; + if (mod.map.isDefine) { + if (mod.exports) { + return mod.exports; + } else { + return (mod.exports = defined[mod.map.id] = {}); + } + } + }, + 'module': function (mod) { + if (mod.module) { + return mod.module; + } else { + return (mod.module = { + id: mod.map.id, + uri: mod.map.url, + config: function () { + var c, + pkg = getOwn(config.pkgs, mod.map.id); + // For packages, only support config targeted + // at the main module. + c = pkg ? getOwn(config.config, mod.map.id + '/' + pkg.main) : + getOwn(config.config, mod.map.id); + return c || {}; + }, + exports: defined[mod.map.id] + }); + } + } + }; + + function cleanRegistry(id) { + //Clean up machinery used for waiting modules. + delete registry[id]; + delete enabledRegistry[id]; + } + + function breakCycle(mod, traced, processed) { + var id = mod.map.id; + + if (mod.error) { + mod.emit('error', mod.error); + } else { + traced[id] = true; + each(mod.depMaps, function (depMap, i) { + var depId = depMap.id, + dep = getOwn(registry, depId); + + //Only force things that have not completed + //being defined, so still in the registry, + //and only if it has not been matched up + //in the module already. + if (dep && !mod.depMatched[i] && !processed[depId]) { + if (getOwn(traced, depId)) { + mod.defineDep(i, defined[depId]); + mod.check(); //pass false? + } else { + breakCycle(dep, traced, processed); + } + } + }); + processed[id] = true; + } + } + + function checkLoaded() { + var map, modId, err, usingPathFallback, + waitInterval = config.waitSeconds * 1000, + //It is possible to disable the wait interval by using waitSeconds of 0. + expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), + noLoads = [], + reqCalls = [], + stillLoading = false, + needCycleCheck = true; + + //Do not bother if this call was a result of a cycle break. + if (inCheckLoaded) { + return; + } + + inCheckLoaded = true; + + //Figure out the state of all the modules. + eachProp(enabledRegistry, function (mod) { + map = mod.map; + modId = map.id; + + //Skip things that are not enabled or in error state. + if (!mod.enabled) { + return; + } + + if (!map.isDefine) { + reqCalls.push(mod); + } + + if (!mod.error) { + //If the module should be executed, and it has not + //been inited and time is up, remember it. + if (!mod.inited && expired) { + if (hasPathFallback(modId)) { + usingPathFallback = true; + stillLoading = true; + } else { + noLoads.push(modId); + removeScript(modId); + } + } else if (!mod.inited && mod.fetched && map.isDefine) { + stillLoading = true; + if (!map.prefix) { + //No reason to keep looking for unfinished + //loading. If the only stillLoading is a + //plugin resource though, keep going, + //because it may be that a plugin resource + //is waiting on a non-plugin cycle. + return (needCycleCheck = false); + } + } + } + }); + + if (expired && noLoads.length) { + //If wait time expired, throw error of unloaded modules. + err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); + err.contextName = context.contextName; + return onError(err); + } + + //Not expired, check for a cycle. + if (needCycleCheck) { + each(reqCalls, function (mod) { + breakCycle(mod, {}, {}); + }); + } + + //If still waiting on loads, and the waiting load is something + //other than a plugin resource, or there are still outstanding + //scripts, then just try back later. + if ((!expired || usingPathFallback) && stillLoading) { + //Something is still waiting to load. Wait for it, but only + //if a timeout is not already in effect. + if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { + checkLoadedTimeoutId = setTimeout(function () { + checkLoadedTimeoutId = 0; + checkLoaded(); + }, 50); + } + } + + inCheckLoaded = false; + } + + Module = function (map) { + this.events = getOwn(undefEvents, map.id) || {}; + this.map = map; + this.shim = getOwn(config.shim, map.id); + this.depExports = []; + this.depMaps = []; + this.depMatched = []; + this.pluginMaps = {}; + this.depCount = 0; + + /* this.exports this.factory + this.depMaps = [], + this.enabled, this.fetched + */ + }; + + Module.prototype = { + init: function (depMaps, factory, errback, options) { + options = options || {}; + + //Do not do more inits if already done. Can happen if there + //are multiple define calls for the same module. That is not + //a normal, common case, but it is also not unexpected. + if (this.inited) { + return; + } + + this.factory = factory; + + if (errback) { + //Register for errors on this module. + this.on('error', errback); + } else if (this.events.error) { + //If no errback already, but there are error listeners + //on this module, set up an errback to pass to the deps. + errback = bind(this, function (err) { + this.emit('error', err); + }); + } + + //Do a copy of the dependency array, so that + //source inputs are not modified. For example + //"shim" deps are passed in here directly, and + //doing a direct modification of the depMaps array + //would affect that config. + this.depMaps = depMaps && depMaps.slice(0); + + this.errback = errback; + + //Indicate this module has be initialized + this.inited = true; + + this.ignore = options.ignore; + + //Could have option to init this module in enabled mode, + //or could have been previously marked as enabled. However, + //the dependencies are not known until init is called. So + //if enabled previously, now trigger dependencies as enabled. + if (options.enabled || this.enabled) { + //Enable this module and dependencies. + //Will call this.check() + this.enable(); + } else { + this.check(); + } + }, + + defineDep: function (i, depExports) { + //Because of cycles, defined callback for a given + //export can be called more than once. + if (!this.depMatched[i]) { + this.depMatched[i] = true; + this.depCount -= 1; + this.depExports[i] = depExports; + } + }, + + fetch: function () { + if (this.fetched) { + return; + } + this.fetched = true; + + context.startTime = (new Date()).getTime(); + + var map = this.map; + + //If the manager is for a plugin managed resource, + //ask the plugin to load it now. + if (this.shim) { + context.makeRequire(this.map, { + enableBuildCallback: true + })(this.shim.deps || [], bind(this, function () { + return map.prefix ? this.callPlugin() : this.load(); + })); + } else { + //Regular dependency. + return map.prefix ? this.callPlugin() : this.load(); + } + }, + + load: function () { + var url = this.map.url; + + //Regular dependency. + if (!urlFetched[url]) { + urlFetched[url] = true; + context.load(this.map.id, url); + } + }, + + /** + * Checks if the module is ready to define itself, and if so, + * define it. + */ + check: function () { + if (!this.enabled || this.enabling) { + return; + } + + var err, cjsModule, + id = this.map.id, + depExports = this.depExports, + exports = this.exports, + factory = this.factory; + + if (!this.inited) { + this.fetch(); + } else if (this.error) { + this.emit('error', this.error); + } else if (!this.defining) { + //The factory could trigger another require call + //that would result in checking this module to + //define itself again. If already in the process + //of doing that, skip this work. + this.defining = true; + + if (this.depCount < 1 && !this.defined) { + if (isFunction(factory)) { + //If there is an error listener, favor passing + //to that instead of throwing an error. However, + //only do it for define()'d modules. require + //errbacks should not be called for failures in + //their callbacks (#699). However if a global + //onError is set, use that. + if ((this.events.error && this.map.isDefine) || + req.onError !== defaultOnError) { + try { + exports = context.execCb(id, factory, depExports, exports); + } catch (e) { + err = e; + } + } else { + exports = context.execCb(id, factory, depExports, exports); + } + + if (this.map.isDefine) { + //If setting exports via 'module' is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + cjsModule = this.module; + if (cjsModule && + cjsModule.exports !== undefined && + //Make sure it is not already the exports value + cjsModule.exports !== this.exports) { + exports = cjsModule.exports; + } else if (exports === undefined && this.usingExports) { + //exports already set the defined value. + exports = this.exports; + } + } + + if (err) { + err.requireMap = this.map; + err.requireModules = this.map.isDefine ? [this.map.id] : null; + err.requireType = this.map.isDefine ? 'define' : 'require'; + return onError((this.error = err)); + } + + } else { + //Just a literal value + exports = factory; + } + + this.exports = exports; + + if (this.map.isDefine && !this.ignore) { + defined[id] = exports; + + if (req.onResourceLoad) { + req.onResourceLoad(context, this.map, this.depMaps); + } + } + + //Clean up + cleanRegistry(id); + + this.defined = true; + } + + //Finished the define stage. Allow calling check again + //to allow define notifications below in the case of a + //cycle. + this.defining = false; + + if (this.defined && !this.defineEmitted) { + this.defineEmitted = true; + this.emit('defined', this.exports); + this.defineEmitComplete = true; + } + + } + }, + + callPlugin: function () { + var map = this.map, + id = map.id, + //Map already normalized the prefix. + pluginMap = makeModuleMap(map.prefix); + + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(pluginMap); + + on(pluginMap, 'defined', bind(this, function (plugin) { + var load, normalizedMap, normalizedMod, + name = this.map.name, + parentName = this.map.parentMap ? this.map.parentMap.name : null, + localRequire = context.makeRequire(map.parentMap, { + enableBuildCallback: true + }); + + //If current map is not normalized, wait for that + //normalized name to load instead of continuing. + if (this.map.unnormalized) { + //Normalize the ID if the plugin allows it. + if (plugin.normalize) { + name = plugin.normalize(name, function (name) { + return normalize(name, parentName, true); + }) || ''; + } + + //prefix and name should already be normalized, no need + //for applying map config again either. + normalizedMap = makeModuleMap(map.prefix + '!' + name, + this.map.parentMap); + on(normalizedMap, + 'defined', bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true, + ignore: true + }); + })); + + normalizedMod = getOwn(registry, normalizedMap.id); + if (normalizedMod) { + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(normalizedMap); + + if (this.events.error) { + normalizedMod.on('error', bind(this, function (err) { + this.emit('error', err); + })); + } + normalizedMod.enable(); + } + + return; + } + + load = bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true + }); + }); + + load.error = bind(this, function (err) { + this.inited = true; + this.error = err; + err.requireModules = [id]; + + //Remove temp unnormalized modules for this module, + //since they will never be resolved otherwise now. + eachProp(registry, function (mod) { + if (mod.map.id.indexOf(id + '_unnormalized') === 0) { + cleanRegistry(mod.map.id); + } + }); + + onError(err); + }); + + //Allow plugins to load other code without having to know the + //context or how to 'complete' the load. + load.fromText = bind(this, function (text, textAlt) { + /*jslint evil: true */ + var moduleName = map.name, + moduleMap = makeModuleMap(moduleName), + hasInteractive = useInteractive; + + //As of 2.1.0, support just passing the text, to reinforce + //fromText only being called once per resource. Still + //support old style of passing moduleName but discard + //that moduleName in favor of the internal ref. + if (textAlt) { + text = textAlt; + } + + //Turn off interactive script matching for IE for any define + //calls in the text, then turn it back on at the end. + if (hasInteractive) { + useInteractive = false; + } + + //Prime the system by creating a module instance for + //it. + getModule(moduleMap); + + //Transfer any config to this other module. + if (hasProp(config.config, id)) { + config.config[moduleName] = config.config[id]; + } + + try { + req.exec(text); + } catch (e) { + return onError(makeError('fromtexteval', + 'fromText eval for ' + id + + ' failed: ' + e, + e, + [id])); + } + + if (hasInteractive) { + useInteractive = true; + } + + //Mark this as a dependency for the plugin + //resource + this.depMaps.push(moduleMap); + + //Support anonymous modules. + context.completeLoad(moduleName); + + //Bind the value of that module to the value for this + //resource ID. + localRequire([moduleName], load); + }); + + //Use parentName here since the plugin's name is not reliable, + //could be some weird string with no path that actually wants to + //reference the parentName's path. + plugin.load(map.name, localRequire, load, config); + })); + + context.enable(pluginMap, this); + this.pluginMaps[pluginMap.id] = pluginMap; + }, + + enable: function () { + enabledRegistry[this.map.id] = this; + this.enabled = true; + + //Set flag mentioning that the module is enabling, + //so that immediate calls to the defined callbacks + //for dependencies do not trigger inadvertent load + //with the depCount still being zero. + this.enabling = true; + + //Enable each dependency + each(this.depMaps, bind(this, function (depMap, i) { + var id, mod, handler; + + if (typeof depMap === 'string') { + //Dependency needs to be converted to a depMap + //and wired up to this module. + depMap = makeModuleMap(depMap, + (this.map.isDefine ? this.map : this.map.parentMap), + false, + !this.skipMap); + this.depMaps[i] = depMap; + + handler = getOwn(handlers, depMap.id); + + if (handler) { + this.depExports[i] = handler(this); + return; + } + + this.depCount += 1; + + on(depMap, 'defined', bind(this, function (depExports) { + this.defineDep(i, depExports); + this.check(); + })); + + if (this.errback) { + on(depMap, 'error', bind(this, this.errback)); + } + } + + id = depMap.id; + mod = registry[id]; + + //Skip special modules like 'require', 'exports', 'module' + //Also, don't call enable if it is already enabled, + //important in circular dependency cases. + if (!hasProp(handlers, id) && mod && !mod.enabled) { + context.enable(depMap, this); + } + })); + + //Enable each plugin that is used in + //a dependency + eachProp(this.pluginMaps, bind(this, function (pluginMap) { + var mod = getOwn(registry, pluginMap.id); + if (mod && !mod.enabled) { + context.enable(pluginMap, this); + } + })); + + this.enabling = false; + + this.check(); + }, + + on: function (name, cb) { + var cbs = this.events[name]; + if (!cbs) { + cbs = this.events[name] = []; + } + cbs.push(cb); + }, + + emit: function (name, evt) { + each(this.events[name], function (cb) { + cb(evt); + }); + if (name === 'error') { + //Now that the error handler was triggered, remove + //the listeners, since this broken Module instance + //can stay around for a while in the registry. + delete this.events[name]; + } + } + }; + + function callGetModule(args) { + //Skip modules already defined. + if (!hasProp(defined, args[0])) { + getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); + } + } + + function removeListener(node, func, name, ieName) { + //Favor detachEvent because of IE9 + //issue, see attachEvent/addEventListener comment elsewhere + //in this file. + if (node.detachEvent && !isOpera) { + //Probably IE. If not it will throw an error, which will be + //useful to know. + if (ieName) { + node.detachEvent(ieName, func); + } + } else { + node.removeEventListener(name, func, false); + } + } + + /** + * Given an event from a script node, get the requirejs info from it, + * and then removes the event listeners on the node. + * @param {Event} evt + * @returns {Object} + */ + function getScriptData(evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + var node = evt.currentTarget || evt.srcElement; + + //Remove the listeners once here. + removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); + removeListener(node, context.onScriptError, 'error'); + + return { + node: node, + id: node && node.getAttribute('data-requiremodule') + }; + } + + function intakeDefines() { + var args; + + //Any defined modules in the global queue, intake them now. + takeGlobalQueue(); + + //Make sure any remaining defQueue items get properly processed. + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); + } else { + //args are id, deps, factory. Should be normalized by the + //define() function. + callGetModule(args); + } + } + } + + context = { + config: config, + contextName: contextName, + registry: registry, + defined: defined, + urlFetched: urlFetched, + defQueue: defQueue, + Module: Module, + makeModuleMap: makeModuleMap, + nextTick: req.nextTick, + onError: onError, + + /** + * Set a configuration for the context. + * @param {Object} cfg config object to integrate. + */ + configure: function (cfg) { + //Make sure the baseUrl ends in a slash. + if (cfg.baseUrl) { + if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { + cfg.baseUrl += '/'; + } + } + + //Save off the paths and packages since they require special processing, + //they are additive. + var pkgs = config.pkgs, + shim = config.shim, + objs = { + paths: true, + config: true, + map: true + }; + + eachProp(cfg, function (value, prop) { + if (objs[prop]) { + if (prop === 'map') { + if (!config.map) { + config.map = {}; + } + mixin(config[prop], value, true, true); + } else { + mixin(config[prop], value, true); + } + } else { + config[prop] = value; + } + }); + + //Merge shim + if (cfg.shim) { + eachProp(cfg.shim, function (value, id) { + //Normalize the structure + if (isArray(value)) { + value = { + deps: value + }; + } + if ((value.exports || value.init) && !value.exportsFn) { + value.exportsFn = context.makeShimExports(value); + } + shim[id] = value; + }); + config.shim = shim; + } + + //Adjust packages if necessary. + if (cfg.packages) { + each(cfg.packages, function (pkgObj) { + var location; + + pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; + location = pkgObj.location; + + //Create a brand new object on pkgs, since currentPackages can + //be passed in again, and config.pkgs is the internal transformed + //state for all package configs. + pkgs[pkgObj.name] = { + name: pkgObj.name, + location: location || pkgObj.name, + //Remove leading dot in main, so main paths are normalized, + //and remove any trailing .js, since different package + //envs have different conventions: some use a module name, + //some use a file name. + main: (pkgObj.main || 'main') + .replace(currDirRegExp, '') + .replace(jsSuffixRegExp, '') + }; + }); + + //Done with modifications, assing packages back to context config + config.pkgs = pkgs; + } + + //If there are any "waiting to execute" modules in the registry, + //update the maps for them, since their info, like URLs to load, + //may have changed. + eachProp(registry, function (mod, id) { + //If module already has init called, since it is too + //late to modify them, and ignore unnormalized ones + //since they are transient. + if (!mod.inited && !mod.map.unnormalized) { + mod.map = makeModuleMap(id); + } + }); + + //If a deps array or a config callback is specified, then call + //require with those args. This is useful when require is defined as a + //config object before require.js is loaded. + if (cfg.deps || cfg.callback) { + context.require(cfg.deps || [], cfg.callback); + } + }, + + makeShimExports: function (value) { + function fn() { + var ret; + if (value.init) { + ret = value.init.apply(global, arguments); + } + return ret || (value.exports && getGlobal(value.exports)); + } + return fn; + }, + + makeRequire: function (relMap, options) { + options = options || {}; + + function localRequire(deps, callback, errback) { + var id, map, requireMod; + + if (options.enableBuildCallback && callback && isFunction(callback)) { + callback.__requireJsBuild = true; + } + + if (typeof deps === 'string') { + if (isFunction(callback)) { + //Invalid call + return onError(makeError('requireargs', 'Invalid require call'), errback); + } + + //If require|exports|module are requested, get the + //value for them from the special handlers. Caveat: + //this only works while module is being defined. + if (relMap && hasProp(handlers, deps)) { + return handlers[deps](registry[relMap.id]); + } + + //Synchronous access to one module. If require.get is + //available (as in the Node adapter), prefer that. + if (req.get) { + return req.get(context, deps, relMap, localRequire); + } + + //Normalize module name, if it contains . or .. + map = makeModuleMap(deps, relMap, false, true); + id = map.id; + + if (!hasProp(defined, id)) { + return onError(makeError('notloaded', 'Module name "' + + id + + '" has not been loaded yet for context: ' + + contextName + + (relMap ? '' : '. Use require([])'))); + } + return defined[id]; + } + + //Grab defines waiting in the global queue. + intakeDefines(); + + //Mark all the dependencies as needing to be loaded. + context.nextTick(function () { + //Some defines could have been added since the + //require call, collect them. + intakeDefines(); + + requireMod = getModule(makeModuleMap(null, relMap)); + + //Store if map config should be applied to this require + //call for dependencies. + requireMod.skipMap = options.skipMap; + + requireMod.init(deps, callback, errback, { + enabled: true + }); + + checkLoaded(); + }); + + return localRequire; + } + + mixin(localRequire, { + isBrowser: isBrowser, + + /** + * Converts a module name + .extension into an URL path. + * *Requires* the use of a module name. It does not support using + * plain URLs like nameToUrl. + */ + toUrl: function (moduleNamePlusExt) { + var ext, + index = moduleNamePlusExt.lastIndexOf('.'), + segment = moduleNamePlusExt.split('/')[0], + isRelative = segment === '.' || segment === '..'; + + //Have a file extension alias, and it is not the + //dots from a relative path. + if (index !== -1 && (!isRelative || index > 1)) { + ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); + moduleNamePlusExt = moduleNamePlusExt.substring(0, index); + } + + return context.nameToUrl(normalize(moduleNamePlusExt, + relMap && relMap.id, true), ext, true); + }, + + defined: function (id) { + return hasProp(defined, makeModuleMap(id, relMap, false, true).id); + }, + + specified: function (id) { + id = makeModuleMap(id, relMap, false, true).id; + return hasProp(defined, id) || hasProp(registry, id); + } + }); + + //Only allow undef on top level require calls + if (!relMap) { + localRequire.undef = function (id) { + //Bind any waiting define() calls to this context, + //fix for #408 + takeGlobalQueue(); + + var map = makeModuleMap(id, relMap, true), + mod = getOwn(registry, id); + + removeScript(id); + + delete defined[id]; + delete urlFetched[map.url]; + delete undefEvents[id]; + + if (mod) { + //Hold on to listeners in case the + //module will be attempted to be reloaded + //using a different config. + if (mod.events.defined) { + undefEvents[id] = mod.events; + } + + cleanRegistry(id); + } + }; + } + + return localRequire; + }, + + /** + * Called to enable a module if it is still in the registry + * awaiting enablement. A second arg, parent, the parent module, + * is passed in for context, when this method is overriden by + * the optimizer. Not shown here to keep code compact. + */ + enable: function (depMap) { + var mod = getOwn(registry, depMap.id); + if (mod) { + getModule(depMap).enable(); + } + }, + + /** + * Internal method used by environment adapters to complete a load event. + * A load event could be a script load or just a load pass from a synchronous + * load call. + * @param {String} moduleName the name of the module to potentially complete. + */ + completeLoad: function (moduleName) { + var found, args, mod, + shim = getOwn(config.shim, moduleName) || {}, + shExports = shim.exports; + + takeGlobalQueue(); + + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + args[0] = moduleName; + //If already found an anonymous module and bound it + //to this name, then this is some other anon module + //waiting for its completeLoad to fire. + if (found) { + break; + } + found = true; + } else if (args[0] === moduleName) { + //Found matching define call for this script! + found = true; + } + + callGetModule(args); + } + + //Do this after the cycle of callGetModule in case the result + //of those calls/init calls changes the registry. + mod = getOwn(registry, moduleName); + + if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { + if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { + if (hasPathFallback(moduleName)) { + return; + } else { + return onError(makeError('nodefine', + 'No define call for ' + moduleName, + null, + [moduleName])); + } + } else { + //A script that does not call define(), so just simulate + //the call for it. + callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); + } + } + + checkLoaded(); + }, + + /** + * Converts a module name to a file path. Supports cases where + * moduleName may actually be just an URL. + * Note that it **does not** call normalize on the moduleName, + * it is assumed to have already been normalized. This is an + * internal API, not a public one. Use toUrl for the public API. + */ + nameToUrl: function (moduleName, ext, skipExt) { + var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url, + parentPath; + + //If a colon is in the URL, it indicates a protocol is used and it is just + //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) + //or ends with .js, then assume the user meant to use an url and not a module id. + //The slash is important for protocol-less URLs as well as full paths. + if (req.jsExtRegExp.test(moduleName)) { + //Just a plain path, not module name lookup, so just return it. + //Add extension if it is included. This is a bit wonky, only non-.js things pass + //an extension, this method probably needs to be reworked. + url = moduleName + (ext || ''); + } else { + //A module that needs to be converted to a path. + paths = config.paths; + pkgs = config.pkgs; + + syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + pkg = getOwn(pkgs, parentModule); + parentPath = getOwn(paths, parentModule); + if (parentPath) { + //If an array, it means there are a few choices, + //Choose the one that is desired + if (isArray(parentPath)) { + parentPath = parentPath[0]; + } + syms.splice(0, i, parentPath); + break; + } else if (pkg) { + //If module name is just the package name, then looking + //for the main module. + if (moduleName === pkg.name) { + pkgPath = pkg.location + '/' + pkg.main; + } else { + pkgPath = pkg.location; + } + syms.splice(0, i, pkgPath); + break; + } + } + + //Join the path parts together, then figure out if baseUrl is needed. + url = syms.join('/'); + url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js')); + url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; + } + + return config.urlArgs ? url + + ((url.indexOf('?') === -1 ? '?' : '&') + + config.urlArgs) : url; + }, + + //Delegates to req.load. Broken out as a separate function to + //allow overriding in the optimizer. + load: function (id, url) { + req.load(context, id, url); + }, + + /** + * Executes a module callback function. Broken out as a separate function + * solely to allow the build system to sequence the files in the built + * layer in the right sequence. + * + * @private + */ + execCb: function (name, callback, args, exports) { + return callback.apply(exports, args); + }, + + /** + * callback for script loads, used to check status of loading. + * + * @param {Event} evt the event from the browser for the script + * that was loaded. + */ + onScriptLoad: function (evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + if (evt.type === 'load' || + (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { + //Reset interactive script so a script node is not held onto for + //to long. + interactiveScript = null; + + //Pull out the name of the module and the context. + var data = getScriptData(evt); + context.completeLoad(data.id); + } + }, + + /** + * Callback for script errors. + */ + onScriptError: function (evt) { + var data = getScriptData(evt); + if (!hasPathFallback(data.id)) { + return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id])); + } + } + }; + + context.require = context.makeRequire(); + return context; + } + + /** + * Main entry point. + * + * If the only argument to require is a string, then the module that + * is represented by that string is fetched for the appropriate context. + * + * If the first argument is an array, then it will be treated as an array + * of dependency string names to fetch. An optional function callback can + * be specified to execute when all of those dependencies are available. + * + * Make a local req variable to help Caja compliance (it assumes things + * on a require that are not standardized), and to give a short + * name for minification/local scope use. + */ + req = requirejs = function (deps, callback, errback, optional) { + + //Find the right context, use default + var context, config, + contextName = defContextName; + + // Determine if have config object in the call. + if (!isArray(deps) && typeof deps !== 'string') { + // deps is a config object + config = deps; + if (isArray(callback)) { + // Adjust args if there are dependencies + deps = callback; + callback = errback; + errback = optional; + } else { + deps = []; + } + } + + if (config && config.context) { + contextName = config.context; + } + + context = getOwn(contexts, contextName); + if (!context) { + context = contexts[contextName] = req.s.newContext(contextName); + } + + if (config) { + context.configure(config); + } + + return context.require(deps, callback, errback); + }; + + /** + * Support require.config() to make it easier to cooperate with other + * AMD loaders on globally agreed names. + */ + req.config = function (config) { + return req(config); + }; + + /** + * Execute something after the current tick + * of the event loop. Override for other envs + * that have a better solution than setTimeout. + * @param {Function} fn function to execute later. + */ + req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { + setTimeout(fn, 4); + } : function (fn) { fn(); }; + + /** + * Export require as a global, but only if it does not already exist. + */ + if (!require) { + require = req; + } + + req.version = version; + + //Used to filter out dependencies that are already paths. + req.jsExtRegExp = /^\/|:|\?|\.js$/; + req.isBrowser = isBrowser; + s = req.s = { + contexts: contexts, + newContext: newContext + }; + + //Create default context. + req({}); + + //Exports some context-sensitive methods on global require. + each([ + 'toUrl', + 'undef', + 'defined', + 'specified' + ], function (prop) { + //Reference from contexts instead of early binding to default context, + //so that during builds, the latest instance of the default context + //with its config gets used. + req[prop] = function () { + var ctx = contexts[defContextName]; + return ctx.require[prop].apply(ctx, arguments); + }; + }); + + if (isBrowser) { + head = s.head = document.getElementsByTagName('head')[0]; + //If BASE tag is in play, using appendChild is a problem for IE6. + //When that browser dies, this can be removed. Details in this jQuery bug: + //http://dev.jquery.com/ticket/2709 + baseElement = document.getElementsByTagName('base')[0]; + if (baseElement) { + head = s.head = baseElement.parentNode; + } + } + + /** + * Any errors that require explicitly generates will be passed to this + * function. Intercept/override it if you want custom error handling. + * @param {Error} err the error object. + */ + req.onError = defaultOnError; + + /** + * Creates the node for the load command. Only used in browser envs. + */ + req.createNode = function (config, moduleName, url) { + var node = config.xhtml ? + document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : + document.createElement('script'); + node.type = config.scriptType || 'text/javascript'; + node.charset = 'utf-8'; + node.async = true; + return node; + }; + + /** + * Does the request to load a module for the browser case. + * Make this a separate function to allow other environments + * to override it. + * + * @param {Object} context the require context to find state. + * @param {String} moduleName the name of the module. + * @param {Object} url the URL to the module. + */ + req.load = function (context, moduleName, url) { + var config = (context && context.config) || {}, + node; + if (isBrowser) { + //In the browser so use a script tag + node = req.createNode(config, moduleName, url); + + node.setAttribute('data-requirecontext', context.contextName); + node.setAttribute('data-requiremodule', moduleName); + + //Set up load listener. Test attachEvent first because IE9 has + //a subtle issue in its addEventListener and script onload firings + //that do not match the behavior of all other browsers with + //addEventListener support, which fire the onload event for a + //script right after the script execution. See: + //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution + //UNFORTUNATELY Opera implements attachEvent but does not follow the script + //script execution mode. + if (node.attachEvent && + //Check if node.attachEvent is artificially added by custom script or + //natively supported by browser + //read https://github.com/jrburke/requirejs/issues/187 + //if we can NOT find [native code] then it must NOT natively supported. + //in IE8, node.attachEvent does not have toString() + //Note the test for "[native code" with no closing brace, see: + //https://github.com/jrburke/requirejs/issues/273 + !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && + !isOpera) { + //Probably IE. IE (at least 6-8) do not fire + //script onload right after executing the script, so + //we cannot tie the anonymous define call to a name. + //However, IE reports the script as being in 'interactive' + //readyState at the time of the define call. + useInteractive = true; + + node.attachEvent('onreadystatechange', context.onScriptLoad); + //It would be great to add an error handler here to catch + //404s in IE9+. However, onreadystatechange will fire before + //the error handler, so that does not help. If addEventListener + //is used, then IE will fire error before load, but we cannot + //use that pathway given the connect.microsoft.com issue + //mentioned above about not doing the 'script execute, + //then fire the script load event listener before execute + //next script' that other browsers do. + //Best hope: IE10 fixes the issues, + //and then destroys all installs of IE 6-9. + //node.attachEvent('onerror', context.onScriptError); + } else { + node.addEventListener('load', context.onScriptLoad, false); + node.addEventListener('error', context.onScriptError, false); + } + node.src = url; + + //For some cache cases in IE 6-8, the script executes before the end + //of the appendChild execution, so to tie an anonymous define + //call to the module name (which is stored on the node), hold on + //to a reference to this node, but clear after the DOM insertion. + currentlyAddingScript = node; + if (baseElement) { + head.insertBefore(node, baseElement); + } else { + head.appendChild(node); + } + currentlyAddingScript = null; + + return node; + } else if (isWebWorker) { + try { + //In a web worker, use importScripts. This is not a very + //efficient use of importScripts, importScripts will block until + //its script is downloaded and evaluated. However, if web workers + //are in play, the expectation that a build has been done so that + //only one script needs to be loaded anyway. This may need to be + //reevaluated if other use cases become common. + importScripts(url); + + //Account for anonymous modules + context.completeLoad(moduleName); + } catch (e) { + context.onError(makeError('importscripts', + 'importScripts failed for ' + + moduleName + ' at ' + url, + e, + [moduleName])); + } + } + }; + + function getInteractiveScript() { + if (interactiveScript && interactiveScript.readyState === 'interactive') { + return interactiveScript; + } + + eachReverse(scripts(), function (script) { + if (script.readyState === 'interactive') { + return (interactiveScript = script); + } + }); + return interactiveScript; + } + + //Look for a data-main script attribute, which could also adjust the baseUrl. + if (isBrowser && !cfg.skipDataMain) { + //Figure out baseUrl. Get it from the script tag with require.js in it. + eachReverse(scripts(), function (script) { + //Set the 'head' where we can append children by + //using the script's parent. + if (!head) { + head = script.parentNode; + } + + //Look for a data-main attribute to set main script for the page + //to load. If it is there, the path to data main becomes the + //baseUrl, if it is not already set. + dataMain = script.getAttribute('data-main'); + if (dataMain) { + //Preserve dataMain in case it is a path (i.e. contains '?') + mainScript = dataMain; + + //Set final baseUrl if there is not already an explicit one. + if (!cfg.baseUrl) { + //Pull off the directory of data-main for use as the + //baseUrl. + src = mainScript.split('/'); + mainScript = src.pop(); + subPath = src.length ? src.join('/') + '/' : './'; + + cfg.baseUrl = subPath; + } + + //Strip off any trailing .js since mainScript is now + //like a module name. + mainScript = mainScript.replace(jsSuffixRegExp, ''); + + //If mainScript is still a path, fall back to dataMain + if (req.jsExtRegExp.test(mainScript)) { + mainScript = dataMain; + } + + //Put the data-main script in the files to load. + cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; + + return true; + } + }); + } + + /** + * The function that handles definitions of modules. Differs from + * require() in that a string for the module should be the first argument, + * and the function to execute after dependencies are loaded should + * return a value to define the module corresponding to the first argument's + * name. + */ + define = function (name, deps, callback) { + var node, context; + + //Allow for anonymous modules + if (typeof name !== 'string') { + //Adjust args appropriately + callback = deps; + deps = name; + name = null; + } + + //This module may not have dependencies + if (!isArray(deps)) { + callback = deps; + deps = null; + } + + //If no name, and callback is a function, then figure out if it a + //CommonJS thing with dependencies. + if (!deps && isFunction(callback)) { + deps = []; + //Remove comments from the callback string, + //look for require calls, and pull them into the dependencies, + //but only if there are function args. + if (callback.length) { + callback + .toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + + //May be a CommonJS thing even without require calls, but still + //could use exports, and module. Avoid doing exports and module + //work though if it just needs require. + //REQUIRES the function to expect the CommonJS variables in the + //order listed below. + deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); + } + } + + //If in IE 6-8 and hit an anonymous define() call, do the interactive + //work. + if (useInteractive) { + node = currentlyAddingScript || getInteractiveScript(); + if (node) { + if (!name) { + name = node.getAttribute('data-requiremodule'); + } + context = contexts[node.getAttribute('data-requirecontext')]; + } + } + + //Always save off evaluating the def call until the script onload handler. + //This allows multiple modules to be in a file without prematurely + //tracing dependencies, and allows for anonymous module support, + //where the module name is not known until the script onload event + //occurs. If no context, use the global queue, and get it processed + //in the onscript load callback. + (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); + }; + + define.amd = { + jQuery: true + }; + + + /** + * Executes the text. Normally just uses eval, but can be modified + * to use a better, environment-specific call. Only used for transpiling + * loader plugins, not for plain JS modules. + * @param {String} text the text to execute/evaluate. + */ + req.exec = function (text) { + /*jslint evil: true */ + return eval(text); + }; + + //Set up with config info. + req(cfg); +}(this)); diff --git a/assets/webconfig/js/vendor/stapes.js b/assets/webconfig/js/vendor/stapes.js new file mode 100644 index 0000000..106af17 --- /dev/null +++ b/assets/webconfig/js/vendor/stapes.js @@ -0,0 +1,594 @@ +// +// ____ _ _ +// / ___|| |_ __ _ _ __ ___ ___ (_)___ (*) +// \___ \| __/ _` | '_ \ / _ \/ __| | / __| +// ___) | || (_| | |_) | __/\__ \_ | \__ \ +// |____/ \__\__,_| .__/ \___||___(_)/ |___/ +// |_| |__/ +// +// (*) a (really) tiny Javascript MVC microframework +// +// (c) Hay Kranen < hay@bykr.org > +// Released under the terms of the MIT license +// < http://en.wikipedia.org/wiki/MIT_License > +// +// Stapes.js : http://hay.github.com/stapes +(function() { + 'use strict'; + + var VERSION = "0.8.0"; + + // Global counter for all events in all modules (including mixed in objects) + var guid = 1; + + // Makes _.create() faster + if (!Object.create) { + var CachedFunction = function(){}; + } + + // So we can use slice.call for arguments later on + var slice = Array.prototype.slice; + + // Private attributes and helper functions, stored in an object so they + // are overwritable by plugins + var _ = { + // Properties + attributes : {}, + + eventHandlers : { + "-1" : {} // '-1' is used for the global event handling + }, + + guid : -1, + + // Methods + addEvent : function(event) { + // If we don't have any handlers for this type of event, add a new + // array we can use to push new handlers + if (!_.eventHandlers[event.guid][event.type]) { + _.eventHandlers[event.guid][event.type] = []; + } + + // Push an event object + _.eventHandlers[event.guid][event.type].push({ + "guid" : event.guid, + "handler" : event.handler, + "scope" : event.scope, + "type" : event.type + }); + }, + + addEventHandler : function(argTypeOrMap, argHandlerOrScope, argScope) { + var eventMap = {}, + scope; + + if (typeof argTypeOrMap === "string") { + scope = argScope || false; + eventMap[ argTypeOrMap ] = argHandlerOrScope; + } else { + scope = argHandlerOrScope || false; + eventMap = argTypeOrMap; + } + + for (var eventString in eventMap) { + var handler = eventMap[eventString]; + var events = eventString.split(" "); + + for (var i = 0, l = events.length; i < l; i++) { + var eventType = events[i]; + _.addEvent.call(this, { + "guid" : this._guid || this._.guid, + "handler" : handler, + "scope" : scope, + "type" : eventType + }); + } + } + }, + + addGuid : function(object, forceGuid) { + if (object._guid && !forceGuid) return; + + object._guid = guid++; + + _.attributes[object._guid] = {}; + _.eventHandlers[object._guid] = {}; + }, + + // This is a really small utility function to save typing and produce + // better optimized code + attr : function(guid) { + return _.attributes[guid]; + }, + + clone : function(obj) { + var type = _.typeOf(obj); + + if (type === 'object') { + return _.extend({}, obj); + } + + if (type === 'array') { + return obj.slice(0); + } + }, + + create : function(proto) { + if (Object.create) { + return Object.create(proto); + } else { + CachedFunction.prototype = proto; + return new CachedFunction(); + } + }, + + createSubclass : function(props, includeEvents) { + props = props || {}; + includeEvents = includeEvents || false; + + var superclass = props.superclass.prototype; + + // Objects always have a constructor, so we need to be sure this is + // a property instead of something from the prototype + var realConstructor = props.hasOwnProperty('constructor') ? props.constructor : function(){}; + + function constructor() { + // Be kind to people forgetting new + if (!(this instanceof constructor)) { + throw new Error("Please use 'new' when initializing Stapes classes"); + } + + // If this class has events add a GUID as well + if (this.on) { + _.addGuid( this, true ); + } + + realConstructor.apply(this, arguments); + } + + if (includeEvents) { + _.extend(superclass, Events); + } + + constructor.prototype = _.create(superclass); + constructor.prototype.constructor = constructor; + + _.extend(constructor, { + extend : function() { + return _.extendThis.apply(this, arguments); + }, + + // We can't call this 'super' because that's a reserved keyword + // and fails in IE8 + 'parent' : superclass, + + proto : function() { + return _.extendThis.apply(this.prototype, arguments); + }, + + subclass : function(obj) { + obj = obj || {}; + obj.superclass = this; + return _.createSubclass(obj); + } + }); + + // Copy all props given in the definition to the prototype + for (var key in props) { + if (key !== 'constructor' && key !== 'superclass') { + constructor.prototype[key] = props[key]; + } + } + + return constructor; + }, + + emitEvents : function(type, data, explicitType, explicitGuid) { + explicitType = explicitType || false; + explicitGuid = explicitGuid || this._guid; + + // #30: make a local copy of handlers to prevent problems with + // unbinding the event while unwinding the loop + var handlers = slice.call(_.eventHandlers[explicitGuid][type]); + + for (var i = 0, l = handlers.length; i < l; i++) { + // Clone the event to prevent issue #19 + var event = _.extend({}, handlers[i]); + var scope = (event.scope) ? event.scope : this; + + if (explicitType) { + event.type = explicitType; + } + + event.scope = scope; + event.handler.call(event.scope, data, event); + } + }, + + // Extend an object with more objects + extend : function() { + var args = slice.call(arguments); + var object = args.shift(); + + for (var i = 0, l = args.length; i < l; i++) { + var props = args[i]; + for (var key in props) { + object[key] = props[key]; + } + } + + return object; + }, + + // The same as extend, but uses the this value as the scope + extendThis : function() { + var args = slice.call(arguments); + args.unshift(this); + return _.extend.apply(this, args); + }, + + // from http://stackoverflow.com/a/2117523/152809 + makeUuid : function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + + removeAttribute : function(keys, silent) { + silent = silent || false; + + // Split the key, maybe we want to remove more than one item + var attributes = _.trim(keys).split(" "); + + // Actually delete the item + for (var i = 0, l = attributes.length; i < l; i++) { + var key = _.trim(attributes[i]); + + if (key) { + delete _.attr(this._guid)[key]; + + // If 'silent' is set, do not throw any events + if (!silent) { + this.emit('change', key); + this.emit('change:' + key); + this.emit('remove', key); + this.emit('remove:' + key); + } + } + } + }, + + removeEventHandler : function(type, handler) { + var handlers = _.eventHandlers[this._guid]; + + if (type && handler) { + // Remove a specific handler + handlers = handlers[type]; + if (!handlers) return; + + for (var i = 0, l = handlers.length, h; i < l; i++) { + h = handlers[i].handler; + if (h && h === handler) { + handlers.splice(i--, 1); + l--; + } + } + } else if (type) { + // Remove all handlers for a specific type + delete handlers[type]; + } else { + // Remove all handlers for this module + _.eventHandlers[this._guid] = {}; + } + }, + + setAttribute : function(key, value, silent) { + silent = silent || false; + + // We need to do this before we actually add the item :) + var itemExists = this.has(key); + var oldValue = _.attr(this._guid)[key]; + + // Is the value different than the oldValue? If not, ignore this call + if (value === oldValue) { + return; + } + + // Actually add the item to the attributes + _.attr(this._guid)[key] = value; + + // If 'silent' flag is set, do not throw any events + if (silent) { + return; + } + + // Throw a generic event + this.emit('change', key); + + // And a namespaced event as well, NOTE that we pass value instead of + // key here! + this.emit('change:' + key, value); + + // Throw namespaced and non-namespaced 'mutate' events as well with + // the old value data as well and some extra metadata such as the key + var mutateData = { + "key" : key, + "newValue" : value, + "oldValue" : oldValue || null + }; + + this.emit('mutate', mutateData); + this.emit('mutate:' + key, mutateData); + + // Also throw a specific event for this type of set + var specificEvent = itemExists ? 'update' : 'create'; + + this.emit(specificEvent, key); + + // And a namespaced event as well, NOTE that we pass value instead of key + this.emit(specificEvent + ':' + key, value); + }, + + trim : function(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }, + + typeOf : function(val) { + if (val === null || typeof val === "undefined") { + // This is a special exception for IE, in other browsers the + // method below works all the time + return String(val); + } else { + return Object.prototype.toString.call(val).replace(/\[object |\]/g, '').toLowerCase(); + } + }, + + updateAttribute : function(key, fn, silent) { + var item = this.get(key); + + // In previous versions of Stapes we didn't have the check for object, + // but still this worked. In 0.7.0 it suddenly doesn't work anymore and + // we need the check. Why? I have no clue. + var type = _.typeOf(item); + + if (type === 'object' || type === 'array') { + item = _.clone(item); + } + + var newValue = fn.call(this, item, key); + _.setAttribute.call(this, key, newValue, silent || false); + } + }; + + // Can be mixed in later using Stapes.mixinEvents(object); + var Events = { + emit : function(types, data) { + data = (typeof data === "undefined") ? null : data; + + var splittedTypes = types.split(" "); + + for (var i = 0, l = splittedTypes.length; i < l; i++) { + var type = splittedTypes[i]; + + // First 'all' type events: is there an 'all' handler in the + // global stack? + if (_.eventHandlers[-1].all) { + _.emitEvents.call(this, "all", data, type, -1); + } + + // Catch all events for this type? + if (_.eventHandlers[-1][type]) { + _.emitEvents.call(this, type, data, type, -1); + } + + if (typeof this._guid === 'number') { + // 'all' event for this specific module? + if (_.eventHandlers[this._guid].all) { + _.emitEvents.call(this, "all", data, type); + } + + // Finally, normal events :) + if (_.eventHandlers[this._guid][type]) { + _.emitEvents.call(this, type, data); + } + } + } + }, + + off : function() { + _.removeEventHandler.apply(this, arguments); + }, + + on : function() { + _.addEventHandler.apply(this, arguments); + } + }; + + _.Module = function() { + + }; + + _.Module.prototype = { + each : function(fn, ctx) { + var attr = _.attr(this._guid); + for (var key in attr) { + var value = attr[key]; + fn.call(ctx || this, value, key); + } + }, + + extend : function() { + return _.extendThis.apply(this, arguments); + }, + + filter : function(fn) { + var filtered = []; + var attributes = _.attr(this._guid); + + for (var key in attributes) { + if ( fn.call(this, attributes[key], key)) { + filtered.push( attributes[key] ); + } + } + + return filtered; + }, + + get : function(input) { + if (typeof input === "string") { + return this.has(input) ? _.attr(this._guid)[input] : null; + } else if (typeof input === "function") { + var items = this.filter(input); + return (items.length) ? items[0] : null; + } + }, + + getAll : function() { + return _.clone( _.attr(this._guid) ); + }, + + getAllAsArray : function() { + var arr = []; + var attributes = _.attr(this._guid); + + for (var key in attributes) { + var value = attributes[key]; + + if (_.typeOf(value) === "object" && !value.id) { + value.id = key; + } + + arr.push(value); + } + + return arr; + }, + + has : function(key) { + return (typeof _.attr(this._guid)[key] !== "undefined"); + }, + + map : function(fn, ctx) { + var mapped = []; + this.each(function(value, key) { + mapped.push( fn.call(ctx || this, value, key) ); + }, ctx || this); + return mapped; + }, + + // Akin to set(), but makes a unique id + push : function(input, silent) { + if (_.typeOf(input) === "array") { + for (var i = 0, l = input.length; i < l; i++) { + _.setAttribute.call(this, _.makeUuid(), input[i], silent || false); + } + } else { + _.setAttribute.call(this, _.makeUuid(), input, silent || false); + } + + return this; + }, + + remove : function(input, silent) { + if (typeof input === 'undefined') { + // With no arguments, remove deletes all attributes + _.attributes[this._guid] = {}; + this.emit('change remove'); + } else if (typeof input === "function") { + this.each(function(item, key) { + if (input(item)) { + _.removeAttribute.call(this, key, silent); + } + }); + } else { + // nb: checking for exists happens in removeAttribute + _.removeAttribute.call(this, input, silent || false); + } + + return this; + }, + + set : function(objOrKey, valueOrSilent, silent) { + if (typeof objOrKey === "object") { + for (var key in objOrKey) { + _.setAttribute.call(this, key, objOrKey[key], valueOrSilent || false); + } + } else { + _.setAttribute.call(this, objOrKey, valueOrSilent, silent || false); + } + + return this; + }, + + size : function() { + var size = 0; + var attr = _.attr(this._guid); + + for (var key in attr) { + size++; + } + + return size; + }, + + update : function(keyOrFn, fn, silent) { + if (typeof keyOrFn === "string") { + _.updateAttribute.call(this, keyOrFn, fn, silent || false); + } else if (typeof keyOrFn === "function") { + this.each(function(value, key) { + _.updateAttribute.call(this, key, keyOrFn); + }); + } + + return this; + } + }; + + var Stapes = { + "_" : _, // private helper functions and properties + + "extend" : function() { + return _.extendThis.apply(_.Module.prototype, arguments); + }, + + "mixinEvents" : function(obj) { + obj = obj || {}; + + _.addGuid(obj); + + return _.extend(obj, Events); + }, + + "on" : function() { + _.addEventHandler.apply(this, arguments); + }, + + "subclass" : function(obj, classOnly) { + classOnly = classOnly || false; + obj = obj || {}; + obj.superclass = classOnly ? function(){} : _.Module; + return _.createSubclass(obj, !classOnly); + }, + + "version" : VERSION + }; + + // This library can be used as an AMD module, a Node.js module, or an + // old fashioned global + if (typeof exports !== "undefined") { + // Server + if (typeof module !== "undefined" && module.exports) { + exports = module.exports = Stapes; + } + exports.Stapes = Stapes; + } else if (typeof define === "function" && define.amd) { + // AMD + define(function() { + return Stapes; + }); + } else { + // Global scope + window.Stapes = Stapes; + } +})(); diff --git a/assets/webconfig/js/vendor/tinycolor.js b/assets/webconfig/js/vendor/tinycolor.js new file mode 100644 index 0000000..4c6f8d9 --- /dev/null +++ b/assets/webconfig/js/vendor/tinycolor.js @@ -0,0 +1,1107 @@ +// TinyColor v1.0.0 +// https://github.com/bgrins/TinyColor +// Brian Grinstead, MIT License + +(function() { + + var trimLeft = /^[\s,#]+/, + trimRight = /\s+$/, + tinyCounter = 0, + math = Math, + mathRound = math.round, + mathMin = math.min, + mathMax = math.max, + mathRandom = math.random; + + var tinycolor = function tinycolor (color, opts) { + + color = (color) ? color : ''; + opts = opts || { }; + + // If input is already a tinycolor, return itself + if (color instanceof tinycolor) { + return color; + } + // If we are called as a function, call using new instead + if (!(this instanceof tinycolor)) { + return new tinycolor(color, opts); + } + + var rgb = inputToRGB(color); + this._r = rgb.r, + this._g = rgb.g, + this._b = rgb.b, + this._a = rgb.a, + this._roundA = mathRound(100*this._a) / 100, + this._format = opts.format || rgb.format; + this._gradientType = opts.gradientType; + + // Don't let the range of [0,255] come back in [0,1]. + // Potentially lose a little bit of precision here, but will fix issues where + // .5 gets interpreted as half of the total, instead of half of 1 + // If it was supposed to be 128, this was already taken care of by `inputToRgb` + if (this._r < 1) { this._r = mathRound(this._r); } + if (this._g < 1) { this._g = mathRound(this._g); } + if (this._b < 1) { this._b = mathRound(this._b); } + + this._ok = rgb.ok; + this._tc_id = tinyCounter++; + }; + + tinycolor.prototype = { + isDark: function() { + return this.getBrightness() < 128; + }, + isLight: function() { + return !this.isDark(); + }, + isValid: function() { + return this._ok; + }, + getFormat: function() { + return this._format; + }, + getAlpha: function() { + return this._a; + }, + getBrightness: function() { + var rgb = this.toRgb(); + return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; + }, + setAlpha: function(value) { + this._a = boundAlpha(value); + this._roundA = mathRound(100*this._a) / 100; + return this; + }, + toHsv: function() { + var hsv = rgbToHsv(this._r, this._g, this._b); + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a }; + }, + toHsvString: function() { + var hsv = rgbToHsv(this._r, this._g, this._b); + var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100); + return (this._a == 1) ? + "hsv(" + h + ", " + s + "%, " + v + "%)" : + "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")"; + }, + toHsl: function() { + var hsl = rgbToHsl(this._r, this._g, this._b); + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a }; + }, + toHslString: function() { + var hsl = rgbToHsl(this._r, this._g, this._b); + var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100); + return (this._a == 1) ? + "hsl(" + h + ", " + s + "%, " + l + "%)" : + "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")"; + }, + toHex: function(allow3Char) { + return rgbToHex(this._r, this._g, this._b, allow3Char); + }, + toHexString: function(allow3Char) { + return '#' + this.toHex(allow3Char); + }, + toHex8: function() { + return rgbaToHex(this._r, this._g, this._b, this._a); + }, + toHex8String: function() { + return '#' + this.toHex8(); + }, + toRgb: function() { + return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a }; + }, + toRgbString: function() { + return (this._a == 1) ? + "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" : + "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")"; + }, + toPercentageRgb: function() { + return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a }; + }, + toPercentageRgbString: function() { + return (this._a == 1) ? + "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" : + "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")"; + }, + toName: function() { + if (this._a === 0) { + return "transparent"; + } + + if (this._a < 1) { + return false; + } + + return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false; + }, + toFilter: function(secondColor) { + var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a); + var secondHex8String = hex8String; + var gradientType = this._gradientType ? "GradientType = 1, " : ""; + + if (secondColor) { + var s = tinycolor(secondColor); + secondHex8String = s.toHex8String(); + } + + return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")"; + }, + toString: function(format) { + var formatSet = !!format; + format = format || this._format; + + var formattedString = false; + var hasAlpha = this._a < 1 && this._a >= 0; + var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name"); + + if (needsAlphaFormat) { + // Special case for "transparent", all other non-alpha formats + // will return rgba when there is transparency. + if (format === "name" && this._a === 0) { + return this.toName(); + } + return this.toRgbString(); + } + if (format === "rgb") { + formattedString = this.toRgbString(); + } + if (format === "prgb") { + formattedString = this.toPercentageRgbString(); + } + if (format === "hex" || format === "hex6") { + formattedString = this.toHexString(); + } + if (format === "hex3") { + formattedString = this.toHexString(true); + } + if (format === "hex8") { + formattedString = this.toHex8String(); + } + if (format === "name") { + formattedString = this.toName(); + } + if (format === "hsl") { + formattedString = this.toHslString(); + } + if (format === "hsv") { + formattedString = this.toHsvString(); + } + + return formattedString || this.toHexString(); + }, + + _applyModification: function(fn, args) { + var color = fn.apply(null, [this].concat([].slice.call(args))); + this._r = color._r; + this._g = color._g; + this._b = color._b; + this.setAlpha(color._a); + return this; + }, + lighten: function() { + return this._applyModification(lighten, arguments); + }, + brighten: function() { + return this._applyModification(brighten, arguments); + }, + darken: function() { + return this._applyModification(darken, arguments); + }, + desaturate: function() { + return this._applyModification(desaturate, arguments); + }, + saturate: function() { + return this._applyModification(saturate, arguments); + }, + greyscale: function() { + return this._applyModification(greyscale, arguments); + }, + spin: function() { + return this._applyModification(spin, arguments); + }, + + _applyCombination: function(fn, args) { + return fn.apply(null, [this].concat([].slice.call(args))); + }, + analogous: function() { + return this._applyCombination(analogous, arguments); + }, + complement: function() { + return this._applyCombination(complement, arguments); + }, + monochromatic: function() { + return this._applyCombination(monochromatic, arguments); + }, + splitcomplement: function() { + return this._applyCombination(splitcomplement, arguments); + }, + triad: function() { + return this._applyCombination(triad, arguments); + }, + tetrad: function() { + return this._applyCombination(tetrad, arguments); + } + }; + + // If input is an object, force 1 into "1.0" to handle ratios properly + // String input requires "1.0" as input, so 1 will be treated as 1 + tinycolor.fromRatio = function(color, opts) { + if (typeof color == "object") { + var newColor = {}; + for (var i in color) { + if (color.hasOwnProperty(i)) { + if (i === "a") { + newColor[i] = color[i]; + } + else { + newColor[i] = convertToPercentage(color[i]); + } + } + } + color = newColor; + } + + return tinycolor(color, opts); + }; + + // Given a string or object, convert that input to RGB + // Possible string inputs: + // + // "red" + // "#f00" or "f00" + // "#ff0000" or "ff0000" + // "#ff000000" or "ff000000" + // "rgb 255 0 0" or "rgb (255, 0, 0)" + // "rgb 1.0 0 0" or "rgb (1, 0, 0)" + // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" + // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" + // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" + // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" + // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" + // + function inputToRGB(color) { + + var rgb = { r: 0, g: 0, b: 0 }; + var a = 1; + var ok = false; + var format = false; + + if (typeof color == "string") { + color = stringInputToObject(color); + } + + if (typeof color == "object") { + if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) { + rgb = rgbToRgb(color.r, color.g, color.b); + ok = true; + format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"; + } + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) { + color.s = convertToPercentage(color.s); + color.v = convertToPercentage(color.v); + rgb = hsvToRgb(color.h, color.s, color.v); + ok = true; + format = "hsv"; + } + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) { + color.s = convertToPercentage(color.s); + color.l = convertToPercentage(color.l); + rgb = hslToRgb(color.h, color.s, color.l); + ok = true; + format = "hsl"; + } + + if (color.hasOwnProperty("a")) { + a = color.a; + } + } + + a = boundAlpha(a); + + return { + ok: ok, + format: color.format || format, + r: mathMin(255, mathMax(rgb.r, 0)), + g: mathMin(255, mathMax(rgb.g, 0)), + b: mathMin(255, mathMax(rgb.b, 0)), + a: a + }; + } + + + // Conversion Functions + // -------------------- + + // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: + // + + // `rgbToRgb` + // Handle bounds / percentage checking to conform to CSS color spec + // + // *Assumes:* r, g, b in [0, 255] or [0, 1] + // *Returns:* { r, g, b } in [0, 255] + function rgbToRgb(r, g, b){ + return { + r: bound01(r, 255) * 255, + g: bound01(g, 255) * 255, + b: bound01(b, 255) * 255 + }; + } + + // `rgbToHsl` + // Converts an RGB color value to HSL. + // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] + // *Returns:* { h, s, l } in [0,1] + function rgbToHsl(r, g, b) { + + r = bound01(r, 255); + g = bound01(g, 255); + b = bound01(b, 255); + + var max = mathMax(r, g, b), min = mathMin(r, g, b); + var h, s, l = (max + min) / 2; + + if(max == min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return { h: h, s: s, l: l }; + } + + // `hslToRgb` + // Converts an HSL color value to RGB. + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] + // *Returns:* { r, g, b } in the set [0, 255] + function hslToRgb(h, s, l) { + var r, g, b; + + h = bound01(h, 360); + s = bound01(s, 100); + l = bound01(l, 100); + + function hue2rgb(p, q, t) { + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + if(s === 0) { + r = g = b = l; // achromatic + } + else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { r: r * 255, g: g * 255, b: b * 255 }; + } + + // `rgbToHsv` + // Converts an RGB color value to HSV + // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] + // *Returns:* { h, s, v } in [0,1] + function rgbToHsv(r, g, b) { + + r = bound01(r, 255); + g = bound01(g, 255); + b = bound01(b, 255); + + var max = mathMax(r, g, b), min = mathMin(r, g, b); + var h, s, v = max; + + var d = max - min; + s = max === 0 ? 0 : d / max; + + if(max == min) { + h = 0; // achromatic + } + else { + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h, s: s, v: v }; + } + + // `hsvToRgb` + // Converts an HSV color value to RGB. + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] + // *Returns:* { r, g, b } in the set [0, 255] + function hsvToRgb(h, s, v) { + + h = bound01(h, 360) * 6; + s = bound01(s, 100); + v = bound01(v, 100); + + var i = math.floor(h), + f = h - i, + p = v * (1 - s), + q = v * (1 - f * s), + t = v * (1 - (1 - f) * s), + mod = i % 6, + r = [v, q, p, p, t, v][mod], + g = [t, v, v, q, p, p][mod], + b = [p, p, t, v, v, q][mod]; + + return { r: r * 255, g: g * 255, b: b * 255 }; + } + + // `rgbToHex` + // Converts an RGB color to hex + // Assumes r, g, and b are contained in the set [0, 255] + // Returns a 3 or 6 character hex + function rgbToHex(r, g, b, allow3Char) { + + var hex = [ + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)) + ]; + + // Return a 3 character hex if possible + if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) { + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); + } + + return hex.join(""); + } + // `rgbaToHex` + // Converts an RGBA color plus alpha transparency to hex + // Assumes r, g, b and a are contained in the set [0, 255] + // Returns an 8 character hex + function rgbaToHex(r, g, b, a) { + + var hex = [ + pad2(convertDecimalToHex(a)), + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)) + ]; + + return hex.join(""); + } + + // `equals` + // Can be called with any tinycolor input + tinycolor.equals = function (color1, color2) { + if (!color1 || !color2) { return false; } + return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString(); + }; + tinycolor.random = function() { + return tinycolor.fromRatio({ + r: mathRandom(), + g: mathRandom(), + b: mathRandom() + }); + }; + + + // Modification Functions + // ---------------------- + // Thanks to less.js for some of the basics here + // + + function desaturate(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.s -= amount / 100; + hsl.s = clamp01(hsl.s); + return tinycolor(hsl); + } + + function saturate(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.s += amount / 100; + hsl.s = clamp01(hsl.s); + return tinycolor(hsl); + } + + function greyscale(color) { + return tinycolor(color).desaturate(100); + } + + function lighten (color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.l += amount / 100; + hsl.l = clamp01(hsl.l); + return tinycolor(hsl); + } + + function brighten(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var rgb = tinycolor(color).toRgb(); + rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100)))); + rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100)))); + rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100)))); + return tinycolor(rgb); + } + + function darken (color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.l -= amount / 100; + hsl.l = clamp01(hsl.l); + return tinycolor(hsl); + } + + // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. + // Values outside of this range will be wrapped into this range. + function spin(color, amount) { + var hsl = tinycolor(color).toHsl(); + var hue = (mathRound(hsl.h) + amount) % 360; + hsl.h = hue < 0 ? 360 + hue : hue; + return tinycolor(hsl); + } + + // Combination Functions + // --------------------- + // Thanks to jQuery xColor for some of the ideas behind these + // + + function complement(color) { + var hsl = tinycolor(color).toHsl(); + hsl.h = (hsl.h + 180) % 360; + return tinycolor(hsl); + } + + function triad(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }) + ]; + } + + function tetrad(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }) + ]; + } + + function splitcomplement(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}), + tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l}) + ]; + } + + function analogous(color, results, slices) { + results = results || 6; + slices = slices || 30; + + var hsl = tinycolor(color).toHsl(); + var part = 360 / slices; + var ret = [tinycolor(color)]; + + for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) { + hsl.h = (hsl.h + part) % 360; + ret.push(tinycolor(hsl)); + } + return ret; + } + + function monochromatic(color, results) { + results = results || 6; + var hsv = tinycolor(color).toHsv(); + var h = hsv.h, s = hsv.s, v = hsv.v; + var ret = []; + var modification = 1 / results; + + while (results--) { + ret.push(tinycolor({ h: h, s: s, v: v})); + v = (v + modification) % 1; + } + + return ret; + } + + // Utility Functions + // --------------------- + + tinycolor.mix = function(color1, color2, amount) { + amount = (amount === 0) ? 0 : (amount || 50); + + var rgb1 = tinycolor(color1).toRgb(); + var rgb2 = tinycolor(color2).toRgb(); + + var p = amount / 100; + var w = p * 2 - 1; + var a = rgb2.a - rgb1.a; + + var w1; + + if (w * a == -1) { + w1 = w; + } else { + w1 = (w + a) / (1 + w * a); + } + + w1 = (w1 + 1) / 2; + + var w2 = 1 - w1; + + var rgba = { + r: rgb2.r * w1 + rgb1.r * w2, + g: rgb2.g * w1 + rgb1.g * w2, + b: rgb2.b * w1 + rgb1.b * w2, + a: rgb2.a * p + rgb1.a * (1 - p) + }; + + return tinycolor(rgba); + }; + + + // Readability Functions + // --------------------- + // + + // `readability` + // Analyze the 2 colors and returns an object with the following properties: + // `brightness`: difference in brightness between the two colors + // `color`: difference in color/hue between the two colors + tinycolor.readability = function(color1, color2) { + var c1 = tinycolor(color1); + var c2 = tinycolor(color2); + var rgb1 = c1.toRgb(); + var rgb2 = c2.toRgb(); + var brightnessA = c1.getBrightness(); + var brightnessB = c2.getBrightness(); + var colorDiff = ( + Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) + + Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) + + Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b) + ); + + return { + brightness: Math.abs(brightnessA - brightnessB), + color: colorDiff + }; + }; + + // `readable` + // http://www.w3.org/TR/AERT#color-contrast + // Ensure that foreground and background color combinations provide sufficient contrast. + // *Example* + // tinycolor.isReadable("#000", "#111") => false + tinycolor.isReadable = function(color1, color2) { + var readability = tinycolor.readability(color1, color2); + return readability.brightness > 125 && readability.color > 500; + }; + + // `mostReadable` + // Given a base color and a list of possible foreground or background + // colors for that base, returns the most readable color. + // *Example* + // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000" + tinycolor.mostReadable = function(baseColor, colorList) { + var bestColor = null; + var bestScore = 0; + var bestIsReadable = false; + for (var i=0; i < colorList.length; i++) { + + // We normalize both around the "acceptable" breaking point, + // but rank brightness constrast higher than hue. + + var readability = tinycolor.readability(baseColor, colorList[i]); + var readable = readability.brightness > 125 && readability.color > 500; + var score = 3 * (readability.brightness / 125) + (readability.color / 500); + + if ((readable && ! bestIsReadable) || + (readable && bestIsReadable && score > bestScore) || + ((! readable) && (! bestIsReadable) && score > bestScore)) { + bestIsReadable = readable; + bestScore = score; + bestColor = tinycolor(colorList[i]); + } + } + return bestColor; + }; + + + // Big List of Colors + // ------------------ + // + var names = tinycolor.names = { + aliceblue: "f0f8ff", + antiquewhite: "faebd7", + aqua: "0ff", + aquamarine: "7fffd4", + azure: "f0ffff", + beige: "f5f5dc", + bisque: "ffe4c4", + black: "000", + blanchedalmond: "ffebcd", + blue: "00f", + blueviolet: "8a2be2", + brown: "a52a2a", + burlywood: "deb887", + burntsienna: "ea7e5d", + cadetblue: "5f9ea0", + chartreuse: "7fff00", + chocolate: "d2691e", + coral: "ff7f50", + cornflowerblue: "6495ed", + cornsilk: "fff8dc", + crimson: "dc143c", + cyan: "0ff", + darkblue: "00008b", + darkcyan: "008b8b", + darkgoldenrod: "b8860b", + darkgray: "a9a9a9", + darkgreen: "006400", + darkgrey: "a9a9a9", + darkkhaki: "bdb76b", + darkmagenta: "8b008b", + darkolivegreen: "556b2f", + darkorange: "ff8c00", + darkorchid: "9932cc", + darkred: "8b0000", + darksalmon: "e9967a", + darkseagreen: "8fbc8f", + darkslateblue: "483d8b", + darkslategray: "2f4f4f", + darkslategrey: "2f4f4f", + darkturquoise: "00ced1", + darkviolet: "9400d3", + deeppink: "ff1493", + deepskyblue: "00bfff", + dimgray: "696969", + dimgrey: "696969", + dodgerblue: "1e90ff", + firebrick: "b22222", + floralwhite: "fffaf0", + forestgreen: "228b22", + fuchsia: "f0f", + gainsboro: "dcdcdc", + ghostwhite: "f8f8ff", + gold: "ffd700", + goldenrod: "daa520", + gray: "808080", + green: "008000", + greenyellow: "adff2f", + grey: "808080", + honeydew: "f0fff0", + hotpink: "ff69b4", + indianred: "cd5c5c", + indigo: "4b0082", + ivory: "fffff0", + khaki: "f0e68c", + lavender: "e6e6fa", + lavenderblush: "fff0f5", + lawngreen: "7cfc00", + lemonchiffon: "fffacd", + lightblue: "add8e6", + lightcoral: "f08080", + lightcyan: "e0ffff", + lightgoldenrodyellow: "fafad2", + lightgray: "d3d3d3", + lightgreen: "90ee90", + lightgrey: "d3d3d3", + lightpink: "ffb6c1", + lightsalmon: "ffa07a", + lightseagreen: "20b2aa", + lightskyblue: "87cefa", + lightslategray: "789", + lightslategrey: "789", + lightsteelblue: "b0c4de", + lightyellow: "ffffe0", + lime: "0f0", + limegreen: "32cd32", + linen: "faf0e6", + magenta: "f0f", + maroon: "800000", + mediumaquamarine: "66cdaa", + mediumblue: "0000cd", + mediumorchid: "ba55d3", + mediumpurple: "9370db", + mediumseagreen: "3cb371", + mediumslateblue: "7b68ee", + mediumspringgreen: "00fa9a", + mediumturquoise: "48d1cc", + mediumvioletred: "c71585", + midnightblue: "191970", + mintcream: "f5fffa", + mistyrose: "ffe4e1", + moccasin: "ffe4b5", + navajowhite: "ffdead", + navy: "000080", + oldlace: "fdf5e6", + olive: "808000", + olivedrab: "6b8e23", + orange: "ffa500", + orangered: "ff4500", + orchid: "da70d6", + palegoldenrod: "eee8aa", + palegreen: "98fb98", + paleturquoise: "afeeee", + palevioletred: "db7093", + papayawhip: "ffefd5", + peachpuff: "ffdab9", + peru: "cd853f", + pink: "ffc0cb", + plum: "dda0dd", + powderblue: "b0e0e6", + purple: "800080", + red: "f00", + rosybrown: "bc8f8f", + royalblue: "4169e1", + saddlebrown: "8b4513", + salmon: "fa8072", + sandybrown: "f4a460", + seagreen: "2e8b57", + seashell: "fff5ee", + sienna: "a0522d", + silver: "c0c0c0", + skyblue: "87ceeb", + slateblue: "6a5acd", + slategray: "708090", + slategrey: "708090", + snow: "fffafa", + springgreen: "00ff7f", + steelblue: "4682b4", + tan: "d2b48c", + teal: "008080", + thistle: "d8bfd8", + tomato: "ff6347", + turquoise: "40e0d0", + violet: "ee82ee", + wheat: "f5deb3", + white: "fff", + whitesmoke: "f5f5f5", + yellow: "ff0", + yellowgreen: "9acd32" + }; + + // Make it easy to access colors via `hexNames[hex]` + var hexNames = tinycolor.hexNames = flip(names); + + + // Utilities + // --------- + + // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }` + function flip(o) { + var flipped = { }; + for (var i in o) { + if (o.hasOwnProperty(i)) { + flipped[o[i]] = i; + } + } + return flipped; + } + + // Return a valid alpha value [0,1] with all invalid values being set to 1 + function boundAlpha(a) { + a = parseFloat(a); + + if (isNaN(a) || a < 0 || a > 1) { + a = 1; + } + + return a; + } + + // Take input from [0, n] and return it as [0, 1] + function bound01(n, max) { + if (isOnePointZero(n)) { n = "100%"; } + + var processPercent = isPercentage(n); + n = mathMin(max, mathMax(0, parseFloat(n))); + + // Automatically convert percentage into number + if (processPercent) { + n = parseInt(n * max, 10) / 100; + } + + // Handle floating point rounding errors + if ((math.abs(n - max) < 0.000001)) { + return 1; + } + + // Convert into [0, 1] range if it isn't already + return (n % max) / parseFloat(max); + } + + // Force a number between 0 and 1 + function clamp01(val) { + return mathMin(1, mathMax(0, val)); + } + + // Parse a base-16 hex value into a base-10 integer + function parseIntFromHex(val) { + return parseInt(val, 16); + } + + // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 + // + function isOnePointZero(n) { + return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1; + } + + // Check to see if string passed in is a percentage + function isPercentage(n) { + return typeof n === "string" && n.indexOf('%') != -1; + } + + // Force a hex value to have 2 characters + function pad2(c) { + return c.length == 1 ? '0' + c : '' + c; + } + + // Replace a decimal with it's percentage value + function convertToPercentage(n) { + if (n <= 1) { + n = (n * 100) + "%"; + } + + return n; + } + + // Converts a decimal to a hex value + function convertDecimalToHex(d) { + return Math.round(parseFloat(d) * 255).toString(16); + } + // Converts a hex value to a decimal + function convertHexToDecimal(h) { + return (parseIntFromHex(h) / 255); + } + + var matchers = (function() { + + // + var CSS_INTEGER = "[-\\+]?\\d+%?"; + + // + var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; + + // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. + var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; + + // Actual matching. + // Parentheses and commas are optional, but not required. + // Whitespace can take the place of commas or opening paren + var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; + var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; + + return { + rgb: new RegExp("rgb" + PERMISSIVE_MATCH3), + rgba: new RegExp("rgba" + PERMISSIVE_MATCH4), + hsl: new RegExp("hsl" + PERMISSIVE_MATCH3), + hsla: new RegExp("hsla" + PERMISSIVE_MATCH4), + hsv: new RegExp("hsv" + PERMISSIVE_MATCH3), + hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, + hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ + }; + })(); + + // `stringInputToObject` + // Permissive string parsing. Take in a number of formats, and output an object + // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` + function stringInputToObject(color) { + + color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase(); + var named = false; + if (names[color]) { + color = names[color]; + named = true; + } + else if (color == 'transparent') { + return { r: 0, g: 0, b: 0, a: 0, format: "name" }; + } + + // Try to match string input using regular expressions. + // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] + // Just return an object and let the conversion functions handle that. + // This way the result will be the same whether the tinycolor is initialized with string or object. + var match; + if ((match = matchers.rgb.exec(color))) { + return { r: match[1], g: match[2], b: match[3] }; + } + if ((match = matchers.rgba.exec(color))) { + return { r: match[1], g: match[2], b: match[3], a: match[4] }; + } + if ((match = matchers.hsl.exec(color))) { + return { h: match[1], s: match[2], l: match[3] }; + } + if ((match = matchers.hsla.exec(color))) { + return { h: match[1], s: match[2], l: match[3], a: match[4] }; + } + if ((match = matchers.hsv.exec(color))) { + return { h: match[1], s: match[2], v: match[3] }; + } + if ((match = matchers.hex8.exec(color))) { + return { + a: convertHexToDecimal(match[1]), + r: parseIntFromHex(match[2]), + g: parseIntFromHex(match[3]), + b: parseIntFromHex(match[4]), + format: named ? "name" : "hex8" + }; + } + if ((match = matchers.hex6.exec(color))) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + format: named ? "name" : "hex" + }; + } + if ((match = matchers.hex3.exec(color))) { + return { + r: parseIntFromHex(match[1] + '' + match[1]), + g: parseIntFromHex(match[2] + '' + match[2]), + b: parseIntFromHex(match[3] + '' + match[3]), + format: named ? "name" : "hex" + }; + } + + return false; + } + + // Node: Export function + if (typeof module !== "undefined" && module.exports) { + module.exports = tinycolor; + } + // AMD/requirejs: Define the module + else if (typeof define === 'function' && define.amd) { + define(function () {return tinycolor;}); + } + // Browser: Expose to window + else { + window.tinycolor = tinycolor; + } + +})(); \ No newline at end of file diff --git a/assets/webconfig/manifest.json b/assets/webconfig/manifest.json new file mode 100644 index 0000000..c15d41a --- /dev/null +++ b/assets/webconfig/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "hyperion remote control", + "short_name": "hyperion remote", + "description": "Client side app for controlling the hyperion server over the local network.", + "author": { + "name": "Daniel Wiese", + "email": "gamadril.dev@gmail.com" + }, + "manifest_version": 2, + "version": "0.6.0", + "minimum_chrome_version": "36", + "permissions": [ + "storage", "system.network" + ], + "app": { + "background": { + "scripts": ["js/background.js"] + } + }, + "sockets": { + "tcp": { + "connect": "*" + }, + "udp": { + "send": "*", + "bind": "*" + } + }, + "icons": { + "128": "res/icon_128.png" + } +} diff --git a/assets/webconfig/manifest.webapp b/assets/webconfig/manifest.webapp new file mode 100644 index 0000000..8acc29e --- /dev/null +++ b/assets/webconfig/manifest.webapp @@ -0,0 +1,14 @@ +{ + "name": "hyperion remote control", + "description": "Client side app for controlling the hyperion server over the local network.", + "launch_path": "/hyperion-remote/index.html", + "icons": { + "128": "/hyperion-remote/res/icon_128.png" + }, + "developer": { + "name": "Daniel Wiese", + "email": "gamadril.dev@gmail.com", + "url": "https://github.com/Gamadril" + }, + "default_locale": "en" +} \ No newline at end of file diff --git a/assets/webconfig/res/colorwheel.png b/assets/webconfig/res/colorwheel.png new file mode 100644 index 0000000000000000000000000000000000000000..1db8ad5b2b2b5ee0855c264d1b3fbe352021ded2 GIT binary patch literal 498530 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