diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2ca3b70 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js + +before_install: + - export NODE_ENV=travis-test + - sudo apt-get update + # - sudo apt-get install libusb-1.0-0-dev + - sudo apt-get install libudev-dev + +node_js: + - 0.10 diff --git a/README.md b/README.md index 5992a9c..3c98c3b 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,36 @@ Currently supported: - [IWY Light Master] (http://iwy-light.de/gb/iwy-starter-sets/iwy-color-single-set-9w.html) - [WIFI370] (http://www.wifiledcontroller.com/#!wifi-370-controller/c1s9b) - [Milight] (http://www.milight.com) + - [MilightRF24] (https://github.com/henryk/openmili) + - [Blinkstick] (https://www.blinkstick.com) + - [Hyperion] (https://github.com/tvdzwan/hyperion/wiki) + +## Installation + +To install the plugin on a Debian or Raspbian system libudev-dev must be installed. + + sudo apt-get install libudev-dev ## Configuration -### For IwyMasten and Wifi370 +### For IwyMaster + +``` + { + "id": "some_id", + "name": "some_name", + "class": "IwyMaster", + "addr": "xxx.xxx.xxx.xxx" + } +``` + +### For Wifi370 ``` { "id": "some_id", "name": "some_name", - "class": "IwyMaster | Wifi370", + "class": "Wifi370", "addr": "xxx.xxx.xxx.xxx" } ``` @@ -34,36 +54,54 @@ Currently supported: ### For MilightRF24 -Pluginconfig: ``` { - "plugin": "led-light", - "MilightRF24Port": "/dev/ttyUSB1" - } -``` - -Devices: -``` - "zones": [ - { - "addr": "5927", - "zone": 0, - "send": true, - "receive": true - }, - { - "addr": "485D", - "zone": 0, - "send": true, - "receive": true - } - ] + "id": 'some_id' + "name": 'some_name' + "class": 'MilightRF24' + "port": '/dev/ttyUSB1' + "zones": [ + { + "addr": "5927", + "zone": 0, + "send": true, + "receive": true + }, + { + "addr": "485D", + "zone": 0, + "send": true, + "receive": true + } + ] + } ``` You will get your addr when you just add the parameter MilightRF24Port to your config and switch to the debug output in pimatic and change some settings with your remote. You need for example an arduino nano and connect it to an nrf24 using the standard SPI wiring. Get the sketch from here https://github.com/henryk/openmili and change the CE and CSN pin to your wiring. +### For Blinkstick + +``` + { + "id": "some_id", + "name": "some_name", + "class": "Blinkstick", + "serial": "xxx" // Only required if more than one Blinkstick is connected to the host. + } +``` + +### For Hyperion + +``` + { + "id": "some_id", + "name": "some_name", + "class": "Wifi370", + "addr": "xxx.xxx.xxx.xxx" + } +``` ## Features @@ -73,5 +111,5 @@ Get the sketch from here https://github.com/henryk/openmili and change the CE an - by color picker (in UI) - by name (in rules e.g. red) - by hex (in rules e.g. #00FF00) - - by teampature variable from weather plugin (in rules e.g. $weather.temperature) + - by temperature variable from weather plugin (in rules e.g. $weather.temperature) diff --git a/device-config-schema.coffee b/device-config-schema.coffee index 79f6fe9..910de93 100644 --- a/device-config-schema.coffee +++ b/device-config-schema.coffee @@ -1,8 +1,8 @@ # #led-light-plugin configuration options module.exports = { title: "Led light device config schemas" - LedLight: { - title: "LedLight" + IwyMaster: { + title: "IwyMaster LedLight" type: "object" properties: addr: @@ -20,6 +20,36 @@ module.exports = { description: "Zone [0 - 4], 0 = switches all zones" type: "number" }, + MilightRF24: { + title: "Milight" + type: "object" + properties: + zones: + description: "The switch protocols to use." + type: "array" + default: [] + format: "table" + items: + type: "object" + properties: + addr: + description: "Address of light device" + type: "string" + port: + description: "USB port where the gateway is attached" + type: "string" + zone: + description: "Zone [0 - 4], 0 = switches all zones" + type: "number" + send: + description: "Send commands using this address and zone" + type: "boolean" + default: true + receive: + description: "Respond on received commands using this address and zone" + type: "boolean" + default: true + }, Wifi370: { title: "LedLight" type: "object" @@ -27,5 +57,32 @@ module.exports = { addr: description: "IP-Address of light device" type: "string" + }, + Blinkstick: { + title: "BlinkStick" + type: "object" + properties: + serial: + description: "serial of Blinkstick" + type: "string" + default: "" + }, + DummyLedLight: { + title: "DummyLedLight" + type: "object" + properties: {} + }, + HyperionLedLight: { + title: "Hyperion", + type: "object" + properties: + addr: + description: "IP-Address of hyperion device" + type: "string" + default: "localhost" + port: + description: "Port of hyperion device" + type: "string", + default: "19444" } } diff --git a/devices/base.coffee b/devices/base.coffee index 8560bb3..4a49c1e 100644 --- a/devices/base.coffee +++ b/devices/base.coffee @@ -59,6 +59,11 @@ module.exports = (env) -> params: brightnessValue: type: t.number + changeDimlevelTo: + description: "Sets the level of the dimmer" + params: + dimlevel: + type: t.number constructor: (initState) -> unless @device @@ -67,7 +72,7 @@ module.exports = (env) -> @name = @config.name @id = @config.id - @power = initState?.power or false + @power = initState?.power or 'off' @color = initState?.color or '' @brightness = initState?.brightness or 100 @mode = initState?.mode or false @@ -118,10 +123,11 @@ module.exports = (env) -> turnOff: -> throw new Error "Function 'turnOff' is not implemented!" setColor: -> throw new Error "Function 'setColor' is not implemented!" setWhite: -> throw new Error "Function 'setWhite' is not implemented!" - setBrightness: -> throw new Error "Function 'setBrightness' is not implemented!" + setBrightness: (brightnessValue) -> throw new Error "Function 'setBrightness' is not implemented!" + changeDimlevelTo: (dimLevel) -> @setBrightness(dimLevel) toggle: -> - if @power is 'on' then @turnOn() else @turnOff() + if @power is 'off' then @turnOn() else @turnOff() Promise.resolve() return BaseLedLight diff --git a/devices/blinkstick.coffee b/devices/blinkstick.coffee new file mode 100644 index 0000000..9c94cd2 --- /dev/null +++ b/devices/blinkstick.coffee @@ -0,0 +1,73 @@ +module.exports = (env) -> + Promise = env.require 'bluebird' + _ = require 'lodash' + Color = require 'color' + BaseLedLight = require('./base')(env) + + + class Blinkstick extends BaseLedLight + + constructor: (@config, lastState) -> + nodeBlinkstick = require 'blinkstick' + if @config.serial + @device = new nodeBlinkstick.findBySerial(@config.serial) + else + @device = new nodeBlinkstick.findFirst() + + initState = _.clone lastState + for key, value of lastState + initState[key] = value.value + super(initState) + if @power then @turnOn() else @turnOff() + + _updateState: (attr) -> + state = _.assign @getState(), attr + super null, state + + setMaxValue = (color, brightness) -> + return (color / 255) * (brightness * 2.5) + + turnOn: -> + @_updateState power: true + if @mode + color = Color(@color).rgb() + else + color = + r: 255 + g: 255 + b: 255 + + @device.setColor(setMaxValue(color.r, @brightness), setMaxValue(color.g, @brightness), setMaxValue(color.b, @brightness)) + Promise.resolve() + + turnOff: -> + @_updateState power: false + @device.turnOff() + Promise.resolve() + + setColor: (newColor) -> + color = Color(newColor).rgb() + @_updateState + mode: @COLOR_MODE + color: color + @device.setColor(setMaxValue(color.r, @brightness), setMaxValue(color.g, @brightness), setMaxValue(color.b, @brightness)) if @power + Promise.resolve() + + setWhite: () -> + @_updateState mode: @WHITE_MODE + @device.setColor(setMaxValue(255, @brightness), setMaxValue(255, @brightness), setMaxValue(255, @brightness)) if @power + Promise.resolve() + + setBrightness: (newBrightness) -> + @_updateState brightness: newBrightness + if @mode + color = Color(@color).rgb() + else + color = + r: 255 + g: 255 + b: 255 + @device.setColor(setMaxValue(color.r, newBrightness), setMaxValue(color.g, newBrightness), setMaxValue(color.b, newBrightness)) if @power + Promise.resolve() + + return Blinkstick diff --git a/devices/dummy.coffee b/devices/dummy.coffee new file mode 100644 index 0000000..b1c3171 --- /dev/null +++ b/devices/dummy.coffee @@ -0,0 +1,49 @@ +module.exports = (env) -> + Promise = env.require 'bluebird' + _ = require 'lodash' + Color = require 'color' + BaseLedLight = require('./base')(env) + + class DummyLedLight extends BaseLedLight + + constructor: (@config, lastState) -> + @device = @ + @name = config.name + @id = config.id + @_dimlevel = lastState?.dimlevel?.value or 0 + + initState = _.clone lastState + for key, value of lastState + initState[key] = value.value + super(initState) + if @power then @turnOn() else @turnOff() + + _updateState: (attr) -> + state = _.assign @getState(), attr + super null, state + + turnOn: -> + @_updateState power: true + Promise.resolve() + + turnOff: -> + @_updateState power: false + Promise.resolve() + + setColor: (newColor) -> + color = Color(newColor).rgb() + @_updateState + mode: @COLOR_MODE + color: color + Promise.resolve() + + setWhite: -> + @_updateState mode: @WHITE_MODE + Promise.resolve() + + setBrightness: (newBrightness) -> + @_updateState brightness: newBrightness + Promise.resolve() + + return DummyLedLight + \ No newline at end of file diff --git a/devices/hyperion.coffee b/devices/hyperion.coffee new file mode 100644 index 0000000..cc7c0b0 --- /dev/null +++ b/devices/hyperion.coffee @@ -0,0 +1,100 @@ +module.exports = (env) -> + Promise = env.require 'bluebird' + _ = require 'lodash' + Color = require 'color' + net = require 'net' + eventToPromise = require 'event-to-promise' + BaseLedLight = require('./base')(env) + Hyperion = require 'hyperion-client' + ### + ## implementation based on https://github.com/danimal4326/homebridge-hyperion + ### + class HyperionLedLight extends BaseLedLight + + _connected: false + + constructor: (@config, lastState) -> + @device = @ + @_dimlevel = lastState?.dimlevel?.value or 0 + + initState = _.clone lastState + for key, value of lastState + initState[key] = value.value + super(initState) + + if @power then @turnOn() else @turnOff() + + _updateState: (attr) -> + state = _.assign @getState(), attr + super null, state + + setMaxValue = (color, brightness) -> + return parseInt((color / 255) * (brightness * 2.55), 10) + + # turning on sets color to white + turnOn: -> + @_updateState power: true + if @mode + color = Color(@color) + else + color = Color("#FFFFFF") + @sendColor(color) + Promise.resolve() + + # turning off means setting hyperion back to default state (usually capture) + turnOff: -> + @_updateState power: false + this.connectToHyperion().then( (hyperion) => + hyperion.clear() + ) + Promise.resolve() + + setColor: (newColor) -> + color = Color(newColor).rgb() + @_updateState + mode: @COLOR_MODE + color: color + @sendColor(Color(newColor)) + Promise.resolve() + + setWhite: -> + @_updateState mode: @WHITE_MODE + @setColor("#FFFFFF") + Promise.resolve() + + setBrightness: (newBrightness) -> + @_updateState brightness: newBrightness + if @mode + color = Color(@color) + else + color = Color("#FFFFFF") + @setColor(color) + Promise.resolve() + + sendColor: (newColor) => + color = newColor.rgbArray().map( (value) => + return setMaxValue(value, @brightness) + ) + this.connectToHyperion().then( (hyperion) => + hyperion.setColor(color) + ).catch( (error) => + env.logger.error("Caught error: " + error) + ).done() + + connectToHyperion: (resolve) => + if @_connected + return Promise.resolve(@hyperion) + else + @hyperion = new Hyperion(@config.addr, @config.port) + @hyperion.on 'error', (error) => + env.logger.error("Error connecting to hyperion.") + if (error?) + env.logger.error(error) + @_connected = false + return eventToPromise(@hyperion, "connect").then( => + env.logger.info("Connected to hyperion!") + @_connected = true + return @hyperion + ) + + return HyperionLedLight diff --git a/devices/iwy_master.coffee b/devices/iwy_master.coffee index 4f29f75..b65c483 100644 --- a/devices/iwy_master.coffee +++ b/devices/iwy_master.coffee @@ -7,7 +7,11 @@ module.exports = (env) -> class IwyMaster extends BaseLedLight constructor: (@config) -> - @device = new IwyMasterDriver @config.addr, @config.device + if @config.class is 'Wifi370' + deviceType = IwyMasterDriver.DEVICES.WIFI370 + else + deviceType = IwyMasterDriver.DEVICES.IWY_MASTER + @device = new IwyMasterDriver @config.addr, @config.port, deviceType @device.on 'error', (err) -> env.logger.warn 'light error:', err @@ -46,4 +50,3 @@ module.exports = (env) -> return Promise.resolve() if @brightness is newBrightness @device.setBrightness newBrightness, @_updateState.bind(@) Promise.resolve() - diff --git a/devices/milight.coffee b/devices/milight.coffee index 31a7ae4..7b84f1f 100644 --- a/devices/milight.coffee +++ b/devices/milight.coffee @@ -11,8 +11,11 @@ module.exports = (env) -> constructor: (@config, lastState) -> @device = new nodeMilight.MilightController ip: @config.addr + delayBetweenCommands: 50 + commandRepeat: 2 @zone = @config.zone + @timerIds = {} initState = _.clone lastState for key, value of lastState @@ -24,45 +27,70 @@ module.exports = (env) -> state = _.assign @getState(), attr super null, state - turnOn: -> - @_updateState power: true - @device.sendCommands(nodeMilight.commands.rgbw.on(@zone)) - if @mode - color = Color(@color).rgb() - @device.sendCommands(nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b)) - else - @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) - @device.sendCommands(nodeMilight.commands.rgbw.brightness(@brightness)) + _debounce: (id, fn) -> + if @timerIds[id]? + clearTimeout @timerIds[id] + @timerIds[id] = null + + @timerIds[id] = setTimeout () => + console.log "Warning. timeid is null" if @timerIds[id] is null + @timerIds[id] = null + fn.call @ + , 200 Promise.resolve() + turnOn: -> + @_debounce 'setSwitch', () => + @_updateState power: true + @device.sendCommands(nodeMilight.commands.rgbw.on(@zone)) + if @mode + color = Color(@color).rgb() + @device.sendCommands(nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b)) + else + @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) + @device.sendCommands(nodeMilight.commands.rgbw.brightness(@brightness)) + Promise.resolve() + turnOff: -> - @_updateState power: false - @device.sendCommands(nodeMilight.commands.rgbw.off(@zone)) - Promise.resolve() + @_debounce 'setSwitch', () => + @_updateState power: false + @device.sendCommands(nodeMilight.commands.rgbw.off(@zone)) + Promise.resolve() setColor: (newColor) -> - color = Color(newColor).rgb() - @_updateState - mode: @COLOR_MODE - color: color - @device.sendCommands( - nodeMilight.commands.rgbw.on(@zone), - nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b) - ) if @power - Promise.resolve() + @_debounce 'setColor', () => + color = Color(newColor).rgb() + if color.r == 255 && color.g == 255 && color.b == 255 + return @setWhite() + + @_updateState + mode: @COLOR_MODE + color: color + + @device.sendCommands( + nodeMilight.commands.rgbw.on(@zone), + nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b) + ) if @power + Promise.resolve() setWhite: () -> - @_updateState mode: @WHITE_MODE - @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) if @power - Promise.resolve() + @_debounce 'setWhite', () => + @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) + + @_updateState + mode: @WHITE_MODE + + @setBrightness @brightness + Promise.resolve() setBrightness: (newBrightness) -> - @_updateState brightness: newBrightness - @device.sendCommands( - nodeMilight.commands.rgbw.on(@zone), - nodeMilight.commands.rgbw.brightness(@brightness) - ) if @power + @_debounce 'setBrightness', () => + @_updateState brightness: newBrightness + @device.sendCommands( + nodeMilight.commands.rgbw.on(@zone), + nodeMilight.commands.rgbw.brightness(@brightness) + ) if @power - Promise.resolve() + Promise.resolve() return Milight diff --git a/devices/milightRF24.coffee b/devices/milightRF24.coffee index 31a7ae4..624e98c 100644 --- a/devices/milightRF24.coffee +++ b/devices/milightRF24.coffee @@ -2,67 +2,235 @@ module.exports = (env) -> Promise = env.require 'bluebird' _ = require 'lodash' Color = require 'color' - nodeMilight = require 'node-milight-promise' + nodeMilightRF24 = require 'node-milight-rf24' + Buttons = nodeMilightRF24.RGBWButtons + NodeMilightRF24 = nodeMilightRF24.MilightRF24Controller BaseLedLight = require('./base')(env) + events = require('events') + + # Handles the connection to the arduino (receives and sends messages) + class MilightRF24 extends events.EventEmitter + # singelton gatway connetion + @connectToGateway: (config) -> + unless MilightRF24.instance + MilightRF24.instance = new MilightRF24 config + return MilightRF24.instance + constructor: (@config) -> + self = @ + @gateway = new NodeMilightRF24 + port: @config.port - class Milight extends BaseLedLight + env.logger.debug "Opening #{@config.port}" + @gateway.open() + + events.EventEmitter.call(this); - constructor: (@config, lastState) -> - @device = new nodeMilight.MilightController - ip: @config.addr - - @zone = @config.zone + @gateway.on("Received", (data) -> + env.logger.debug data + + self.emit("ReceivedData", data); + ) + + getGateway: -> + @gateway + + getDevice: (config, lastState) -> + new MilightRF24Zone(config, lastState, @) + + setColor: (id, zone, r,g,b) -> + env.logger.debug "Sending Color. Addr: #{id} Zone: #{zone} Red: #{r} Green: #{g} Blue: #{b}" + @gateway.setColor(id, zone, r,g,b) + + @_loop(id, zone, Buttons.ColorFader, false, 0, 0, r,g,b) + + setBrightness: (id, zone, brightness) -> + env.logger.debug "Sending Brightness. Addr:#{id} Zone:#{zone} Brightness:#{brightness}" + @gateway.setBrightness(id, zone, brightness) + + @_loop(id, zone, Buttons.BrightnessFader, false, 0, brightness, 0,0,0) + + setWhite: (id, zone) -> + env.logger.debug "Sending Whitemode. Addr:#{id} Zone:#{zone}" + switch zone + when 0 + button = Buttons.AllOn + when 1 + button = Buttons.Group1On + when 2 + button = Buttons.Group2On + when 3 + button = Buttons.Group3On + when 4 + button = Buttons.Group4On + + @gateway.sendButton(id, zone, button, true) + + @_loop(id, zone, button, true, 0, 0, 0,0,0) + + turnOn: (id, zone) -> + env.logger.debug "Sending On. Addr:#{id} Zone:#{zone}" + switch zone + when 0 + button = Buttons.AllOn + when 1 + button = Buttons.Group1On + when 2 + button = Buttons.Group2On + when 3 + button = Buttons.Group3On + when 4 + button = Buttons.Group4On + + @gateway.sendButton(id, zone, button, false) + + @_loop(id, zone, button, false, 0, 0, 0,0,0) + + turnOff: (id, zone) -> + env.logger.debug "Sending Off. Addr:#{id} Zone:#{zone}" + switch zone + when 0 + button = Buttons.AllOff + when 1 + button = Buttons.Group1Off + when 2 + button = Buttons.Group2Off + when 3 + button = Buttons.Group3Off + when 4 + button = Buttons.Group4Off + + @gateway.sendButton(id, zone, button, false) + + @_loop(id, zone, button, false, 0, 0, 0,0,0) + + # loop for changes to zone 0 to be reflected by all other zones which have the same id + _loop: (id, zone, button, longPress, discoMode, brightness, r, g, b) -> + dataObj = + raw: "loop", + id: id, + zone: zone, + button: button, + longPress: longPress, + discoMode: discoMode, + brightness: brightness, + color: + r: r, + g: g, + b: b + + @.emit("ReceivedData", dataObj); + + # registers for messages from the main class and checks if incoming messages are addressed at the registered ids and zone combination + # sends changes from the gui to the main class, so that they are send to the arduino + class MilightRF24Zone extends BaseLedLight + constructor: (@config, lastState, MilightRF24Gateway) -> + self = @ + @device = @ + @gateway = MilightRF24Gateway + @zones = @config.zones + @brightness = 100 + @color = "FFFF00" + initState = _.clone lastState for key, value of lastState initState[key] = value.value super(initState) if @power then @turnOn() else @turnOff() + + # register for incoming messages + @gateway.on('ReceivedData', (data) -> + self.zones.forEach (z) -> + + # check if this zone listens on the current zone from config + unless z.receive is false + if z.addr is data.id + if data.button is Buttons.AllOff or (data.button is Buttons.Group1Off and z.zone is 1) or (data.button is Buttons.Group2Off and z.zone is 2) or (data.button is Buttons.Group3Off and z.zone is 3) or (data.button is Buttons.Group4Off and z.zone is 4) + self.turnOff(false) + + if (z.zone is data.zone or data.zone is 0) and data.longPress is true and (Buttons.AllOn or Buttons.Group1On or Buttons.Group2On or Buttons.Group3On or Buttons.Group4On) + self.setWhite(false) + + if z.zone is data.zone or data.zone is 0 + switch data.button + when Buttons.AllOn, Buttons.Group1On, Buttons.Group2On, Buttons.Group3On, Buttons.Group4On + self.turnOn(false) + when Buttons.AllOff + self.turnOff(false) + when Buttons.ColorFader or Buttons.FaderReleased + self.setColor("#"+self._num2Hex(data.color.r)+self._num2Hex(data.color.g)+self._num2Hex(data.color.b), false) + when Buttons.BrightnessFader or Buttons.FaderReleased + self.setBrightness(data.brightness, false) + ) + _num2Hex: (s) -> + a = s.toString(16); + if (a.length % 2) > 0 + a = "0" + a; + a; + _updateState: (attr) -> state = _.assign @getState(), attr super null, state - turnOn: -> + turnOn: (send) -> + self = @ + @_updateState power: true - @device.sendCommands(nodeMilight.commands.rgbw.on(@zone)) - if @mode - color = Color(@color).rgb() - @device.sendCommands(nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b)) - else - @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) - @device.sendCommands(nodeMilight.commands.rgbw.brightness(@brightness)) + + @zones.forEach (z) -> + unless z.send is false or send is false + self.gateway.turnOn(z.addr, z.zone) + + unless z.zone is 0 + if self.mode + color = Color(self.color).rgb() + self.gateway.setColor(z.addr, z.zone, color.r, color.g, color.b, true) + else + self.gateway.setWhite(z.addr, z.zone) + + self.gateway.setBrightness(z.addr, z.zone, self.brightness) + Promise.resolve() - turnOff: -> + turnOff: (send) -> + self = @ @_updateState power: false - @device.sendCommands(nodeMilight.commands.rgbw.off(@zone)) + @zones.forEach (z) -> + unless z.send is false or send is false + self.gateway.turnOff(z.addr, z.zone) Promise.resolve() - setColor: (newColor) -> + setColor: (newColor, send) -> + + self = @ color = Color(newColor).rgb() @_updateState mode: @COLOR_MODE color: color - @device.sendCommands( - nodeMilight.commands.rgbw.on(@zone), - nodeMilight.commands.rgbw.rgb255(color.r, color.g, color.b) - ) if @power + + @zones.forEach (z) -> + unless z.send is false or send is false + self.gateway.setColor(z.addr, z.zone, color.r, color.g, color.b, true) if self.power Promise.resolve() - setWhite: () -> + setWhite: (send) -> + self = @ @_updateState mode: @WHITE_MODE - @device.sendCommands(nodeMilight.commands.rgbw.whiteMode(@zone)) if @power + + @zones.forEach (z) -> + unless z.send is false or send is false + self.gateway.setWhite(z.addr, z.zone) if self.power Promise.resolve() - setBrightness: (newBrightness) -> + setBrightness: (newBrightness, send) -> + self = @ @_updateState brightness: newBrightness - @device.sendCommands( - nodeMilight.commands.rgbw.on(@zone), - nodeMilight.commands.rgbw.brightness(@brightness) - ) if @power + @zones.forEach (z) -> + unless z.send is false or send is false + self.gateway.setBrightness(z.addr, z.zone, newBrightness) if self.power Promise.resolve() - return Milight + return MilightRF24 diff --git a/devices/wifi370.coffee b/devices/wifi370.coffee index d9a2eec..712c1fd 100644 --- a/devices/wifi370.coffee +++ b/devices/wifi370.coffee @@ -1,4 +1,4 @@ -# just ponits to the IWY Master class +# just points to the IWY Master class # in future this file might be used to extend support for WIFI370 specific functionalities module.exports = (env) -> diff --git a/led-light-schema.coffee b/led-light-schema.coffee index 4d53677..f6cd8e8 100644 --- a/led-light-schema.coffee +++ b/led-light-schema.coffee @@ -2,5 +2,5 @@ module.exports = { title: "Plugin config options" type: "object" - properties:{} -} \ No newline at end of file + properties: {} +} diff --git a/package.json b/package.json index b37a50c..0a91bed 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,30 @@ "description": "Control LED lights", "author": "Philip Patzer (https://github.com/philip1986)", "main": "pimatic-led-light", - "version": "0.0.1", + "version": "0.3.2", + "license": "GPL-2.0", "repository": { "type": "git", "url": "git://github.com/philip1986/pimatic-led-light.git" }, + "bugs": { + "url": "https://github.com/philip1986/pimatic-led-light/issues" + }, "configSchema": "led-light-schema.coffee", + "scripts": { + "test": "node_modules/.bin/mocha test" + }, "dependencies": { + "bluebird": "^3.1.1", "cassert": "^0.1.2", "color": "^0.8.0", - "iwy_master": "0.2.2", - "node-milight-promise": ">=0.0.2", - "lodash": "^3.5.0" + "iwy_master": "0.2.3", + "node-milight-promise": ">=0.0.3", + "node-milight-rf24": ">=0.1.1", + "lodash": "^3.10.1", + "blinkstick": "1.1.1", + "hyperion-client": "1.0.0", + "event-to-promise": "0.6.0" }, "peerDependencies": { "pimatic": "0.8.*" @@ -22,5 +34,34 @@ "engines": { "node": ">0.8.x", "npm": ">1.1.x" + }, + "contributors": [ + { + "name": "Markus Minini", + "url": "https://github.com/xento" + }, + { + "name": "Marcus Wittig", + "url": "https://github.com/mwitig" + }, + { + "name": "Anton", + "url": "https://github.com/tester22" + }, + { + "name": "Fjuxx", + "url": "https://github.com/Fjuxx" + }, + { + "name": "Oitzu", + "url": "https://github.com/Oitzu" + } + ], + "devDependencies": { + "coffee-script": "^1.9.3", + "decl-api": "0.0.17", + "mocha": "^2.2.5", + "should": "^7.0.2", + "sinon": "^1.15.4" } } diff --git a/pimatic-led-light.coffee b/pimatic-led-light.coffee index 74524ea..670916c 100644 --- a/pimatic-led-light.coffee +++ b/pimatic-led-light.coffee @@ -3,7 +3,12 @@ module.exports = (env) -> # import device wrappers IwyMaster = require('./devices/iwy_master')(env) Milight = require('./devices/milight')(env) + MilightRF24 = require('./devices/milightRF24')(env) Wifi370 = require('./devices/wifi370')(env) + unless process.env.NODE_ENV is 'travis-test' + Blinkstick = require('./devices/blinkstick')(env) + DummyLedLight = require('./devices/dummy')(env) + HyperionLedLight = require('./devices/hyperion')(env) # import preadicares and actions ColorActionProvider = require('./predicates_and_actions/color_action')(env) @@ -14,7 +19,7 @@ module.exports = (env) -> deviceConfigDef = require('./device-config-schema.coffee') @framework.deviceManager.registerDeviceClass 'IwyMaster', - configDef: deviceConfigDef.LedLight + configDef: deviceConfigDef.IwyMaster createCallback: (config) -> return new IwyMaster(config) @framework.deviceManager.registerDeviceClass 'Wifi370', @@ -25,6 +30,24 @@ module.exports = (env) -> configDef: deviceConfigDef.Milight createCallback: (config, lastState) -> return new Milight(config, lastState) + @framework.deviceManager.registerDeviceClass 'MilightRF24', + configDef: deviceConfigDef.MilightRF24 + createCallback: (config, lastState) -> + return MilightRF24.connectToGateway(config).getDevice(config, lastState) + + unless process.env.NODE_ENV is 'travis-test' + @framework.deviceManager.registerDeviceClass 'Blinkstick', + configDef: deviceConfigDef.Blinkstick + createCallback: (config) -> return new Blinkstick(config) + + @framework.deviceManager.registerDeviceClass 'DummyLedLight', + configDef: deviceConfigDef.DummyLedLight + createCallback: (config) -> return new DummyLedLight(config) + + @framework.deviceManager.registerDeviceClass 'Hyperion', + configDef: deviceConfigDef.HyperionLedLight + createCallback: (config) -> return new HyperionLedLight(config) + @framework.ruleManager.addActionProvider(new ColorActionProvider(@framework)) # wait till all plugins are loaded @@ -37,6 +60,7 @@ module.exports = (env) -> mobileFrontend.registerAssetFile 'html', 'pimatic-led-light/ui/led-light.html' mobileFrontend.registerAssetFile 'js', 'pimatic-led-light/ui/vendor/spectrum.js' mobileFrontend.registerAssetFile 'css', 'pimatic-led-light/ui/vendor/spectrum.css' + mobileFrontend.registerAssetFile 'js', 'pimatic-led-light/ui/vendor/async.js' else env.logger.warn 'your plugin could not find the mobile-frontend. No gui will be available' diff --git a/predicates_and_actions/color_action.coffee b/predicates_and_actions/color_action.coffee index c03ce84..f5b541c 100644 --- a/predicates_and_actions/color_action.coffee +++ b/predicates_and_actions/color_action.coffee @@ -29,16 +29,18 @@ module.exports = (env) -> hexColor += temperatureColor.rgb().g.toString(16) hexColor += temperatureColor.rgb().b.toString(16) - callback hexColor + callback hexColor, simulate else - callback @color + callback @color, simulate - getColor (color) => - if simulate - return Promise.resolve(__("would log set color #{color}")) - else - @device.setColor color - return Promise.resolve(__("set color #{color}")) + getColor @setColor + + setColor: (color, simulate) => + if simulate + return Promise.resolve(__("would log set color #{color}")) + else + @device.setColor color + return Promise.resolve(__("set color #{color}")) class ColorActionProvider extends env.actions.ActionProvider constructor: (@framework) -> @@ -81,7 +83,7 @@ module.exports = (env) -> color = color_schema[s] match = m.getFullMatch() - # color by temprature from variable like $weather.temperature = 30 + # color by temperature from variable like $weather.temperature = 30 (m) -> m.match ['temperature based color by variable '], (m) -> m.matchVariable (m, s) -> diff --git a/test/app_stub/env.coffee b/test/app_stub/env.coffee new file mode 100644 index 0000000..49b6475 --- /dev/null +++ b/test/app_stub/env.coffee @@ -0,0 +1,20 @@ +sinon = require 'sinon' +{ EventEmitter } = require 'events' + +self = @ + +exports.env = + require: (lib) -> require lib + devices: + Device: class Device extends EventEmitter + getDeviceEmitSpy: -> sinon.spy @devices.Device.prototype, 'emit' + actions: + ActionHandler: class ActionHandler + ActionProvider: class ActionProvider + plugins: + Plugin: class Plugin + logger: + debug: sinon.stub() + info: sinon.stub() + warn: sinon.stub() + error: sinon.stub() diff --git a/test/app_stub/framework.coffee b/test/app_stub/framework.coffee new file mode 100644 index 0000000..547de7e --- /dev/null +++ b/test/app_stub/framework.coffee @@ -0,0 +1,29 @@ +should = require 'should' +{ EventEmitter } = require 'events' + +class Framework extends EventEmitter + constructor: (deviceClass, deviceConfig) -> + @device = null + + @deviceManager = + registerDeviceClass: (args...) => + args[0].should.be.type 'string' + args[1].should.have.property 'configDef' + args[1].should.have.property 'createCallback' + + return unless args[0] is deviceClass + @device = args[1].createCallback(deviceConfig) + + @ruleManager = + addActionProvider: -> + @pluginManager = + getPlugin: -> + +exports.loadPluginWithEnvAndConfig = (env, deviceClass, deviceConfig) -> + plugin = new (require '../../pimatic-led-light')(env) + framework = new Framework deviceClass, deviceConfig + + plugin.init null, framework, + plugin: 'led-light' + + return framework.device diff --git a/test/driver_stubs/iwy_master.coffee b/test/driver_stubs/iwy_master.coffee new file mode 100644 index 0000000..9b13359 --- /dev/null +++ b/test/driver_stubs/iwy_master.coffee @@ -0,0 +1,32 @@ +sinon = require 'sinon' +IwyMasterDriver = require 'iwy_master' + +class DriverStub + defaultDeviceState: + power: false + mode: 'WHITE' + brightness: 100 + color: + r: 0 + g: 0 + b: 0 + + @switchOn = sinon.stub IwyMasterDriver.prototype, 'switchOn' + @switchOff = sinon.stub IwyMasterDriver.prototype, 'switchOff' + @getStateStub = sinon.stub IwyMasterDriver.prototype, 'getState' + @setColor = sinon.stub IwyMasterDriver.prototype, 'setColor' + @setWhite = sinon.stub IwyMasterDriver.prototype, 'setWhite' + @setBrightness = sinon.stub IwyMasterDriver.prototype, 'setBrightness' + + @reset: -> + @switchOn.reset() + @switchOff.reset() + @getStateStub.reset() + @setColor.reset() + @setWhite.reset() + @setBrightness.reset() + +### default behavior ### +DriverStub.getStateStub.yields null, DriverStub.defaultDeviceState + +exports.DriverStub = DriverStub diff --git a/test/driver_stubs/milight.coffee b/test/driver_stubs/milight.coffee new file mode 100644 index 0000000..690c235 --- /dev/null +++ b/test/driver_stubs/milight.coffee @@ -0,0 +1,10 @@ +sinon = require 'sinon' +{ MilightController } = require 'node-milight-promise' + +class DriverStub + @sendCommands = sinon.stub MilightController.prototype, 'sendCommands' + + @reset: -> + @sendCommands.reset() + +exports.DriverStub = DriverStub diff --git a/test/driver_stubs/milightRF24.coffee b/test/driver_stubs/milightRF24.coffee new file mode 100644 index 0000000..fb92d90 --- /dev/null +++ b/test/driver_stubs/milightRF24.coffee @@ -0,0 +1,25 @@ +sinon = require 'sinon' +{ EventEmitter } = require 'events' +MilightRF24 = require 'node-milight-rf24' + +# replace whole Controller class +MilightRF24.MilightRF24Controller = class FakeMilightRF24Controller extends EventEmitter + constructor: (@config) -> + open: -> + setColor: -> + setBrightness: -> + sendButton: -> + +class DriverStub + @open = sinon.stub MilightRF24.MilightRF24Controller.prototype, 'open' + @setColor = sinon.stub MilightRF24.MilightRF24Controller.prototype, 'setColor' + @setBrightness = sinon.stub MilightRF24.MilightRF24Controller.prototype, 'setBrightness' + @sendButton = sinon.stub MilightRF24.MilightRF24Controller.prototype, 'sendButton' + + @reset: -> + @open.reset() + @setColor.reset() + @setBrightness.reset() + @sendButton.reset() + +exports.DriverStub = DriverStub diff --git a/test/iwy_master.coffee b/test/iwy_master.coffee new file mode 100644 index 0000000..dc6b1a5 --- /dev/null +++ b/test/iwy_master.coffee @@ -0,0 +1,75 @@ +should = require 'should' +{ env } = require './app_stub/env' +{ loadPluginWithEnvAndConfig } = require './app_stub/framework' +{ DriverStub } = require './driver_stubs/iwy_master' + +describe 'IWY Master', -> + device = null + + config = + id: 'some_id' + name: 'some_name' + class: 'IwyMaster' + addr: '127.0.0.1' + + beforeEach -> + device = loadPluginWithEnvAndConfig env, 'IwyMaster', config + + afterEach -> + DriverStub.reset() + + describe '#getPower', -> + it 'should return the current power state (false by default)', (done) -> + device.getPower().then (power) -> + power.should.equal 'off' + done() + + describe '#getMode', -> + it 'should return the current power state (white by default)', (done) -> + device.getMode().then (mode) -> + mode.should.equal false + done() + + describe '#turnOn', -> + it 'should call the corresponding driver method', -> + device.turnOn() + DriverStub.switchOn.calledOnce.should.equal true + + describe '#turnOff', -> + it 'should call the corresponding driver method', -> + device.power = 'on' + device.turnOff() + DriverStub.switchOff.calledOnce.should.equal true + + describe '#toggle', -> + it 'should switch the power state to ON when it is OFF before', -> + device.power = 'off' + device.toggle() + DriverStub.switchOn.calledOnce.should.equal true + DriverStub.switchOff.calledOnce.should.equal false + + it 'should switch the power state to OFF when it is ON before', -> + device.power = 'on' + device.toggle() + DriverStub.switchOn.calledOnce.should.equal false + DriverStub.switchOff.calledOnce.should.equal true + + describe '#setWhite', -> + it 'should call the corresponding driver method', -> + device.setWhite() + DriverStub.setWhite.calledOnce.should.equal true + + describe '#setColor', -> + it 'should call the corresponding driver method', -> + device.setColor('#FFFFFF') + DriverStub.setColor.calledOnce.should.equal true + + describe '#setBrightness', -> + it 'should call the corresponding driver method', -> + device.setBrightness(50) + DriverStub.setBrightness.calledOnce.should.equal true + + describe '#changeDimlevelTo', -> + it 'should call the corresponding driver method', -> + device.changeDimlevelTo(50) + DriverStub.setBrightness.calledOnce.should.equal true diff --git a/test/milight.coffee b/test/milight.coffee new file mode 100644 index 0000000..81dcb02 --- /dev/null +++ b/test/milight.coffee @@ -0,0 +1,125 @@ +should = require 'should' +{ env } = require './app_stub/env' +{ loadPluginWithEnvAndConfig } = require './app_stub/framework' +{ DriverStub } = require './driver_stubs/milight' +nodeMilight = require 'node-milight-promise' + +describe 'Milight', -> + device = null + + config = + id: 'some_id' + name: 'some_name' + class: 'Milight' + addr: '127.0.0.1' + zone: 1 + + beforeEach -> + device = loadPluginWithEnvAndConfig env, 'Milight', config + DriverStub.reset() + + # set default state + device.mode = false + device.color = '' + device.power = 'off' + device.brightness = 100 + + + describe '#getPower', -> + it 'should return the current power state (off by default)', (done) -> + device.getPower().then (power) -> + power.should.equal 'off' + done() + + describe '#getMode', -> + it 'should return the current power state (white (false) by default)', (done) -> + device.getMode().then (mode) -> + mode.should.equal false + done() + + describe '#turnOn', -> + it 'should send the corresponding driver commands', -> + device.turnOn() + + DriverStub.sendCommands.calledThrice.should.equal true + # switch on the device + DriverStub.sendCommands.firstCall.args[0].should.eql nodeMilight.commands.rgbw.on(config.zone) + # set device into white mode + DriverStub.sendCommands.secondCall.args[0].should.eql nodeMilight.commands.rgbw.whiteMode(config.zone) + # set brightness + DriverStub.sendCommands.thirdCall.args[0].should.eql nodeMilight.commands.rgbw.brightness(device.brightness) + + describe '#turnOff', -> + it 'should send the corresponding driver commands', -> + device.turnOff() + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args[0].should.eql nodeMilight.commands.rgbw.off(config.zone) + + describe '#toggle', -> + it 'should switch the power state to ON when it is OFF before', -> + device.power = 'off' + device.toggle() + + DriverStub.sendCommands.calledThrice.should.equal true + # just check that switch ON command is fired + DriverStub.sendCommands.firstCall.args[0].should.eql nodeMilight.commands.rgbw.on(config.zone) + + it 'should switch the power state to OFF when it is ON before', -> + device.power = 'on' + device.toggle() + + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args[0].should.eql nodeMilight.commands.rgbw.off(config.zone) + + describe '#setWhite', -> + it 'should call the corresponding driver method', -> + device.setWhite() + + DriverStub.sendCommands.calledTwice.should.equal true + # first switch device to white mode + DriverStub.sendCommands.firstCall.args[0].should.eql nodeMilight.commands.rgbw.whiteMode(config.zone) + # second switch on (first argument) and set brightness (second argument) + DriverStub.sendCommands.secondCall.args[0].should.eql nodeMilight.commands.rgbw.on(config.zone) + DriverStub.sendCommands.secondCall.args[1].should.eql nodeMilight.commands.rgbw.brightness(device.brightness) + + describe '#setColor', -> + it 'should call the corresponding driver method', -> + device.setColor('#AAAAAA') + device.power = 'on' + + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args.should.eql [ + nodeMilight.commands.rgbw.on(config.zone) + nodeMilight.commands.rgbw.rgb255(Number('0xAA'), Number('0xAA'), Number('0xAA')) + ] + + context 'device power is "off"', -> + it 'should call the corresponding driver method', -> + device.setColor('#AAAAAA') + device.power = 'off' + + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args.should.eql [ + nodeMilight.commands.rgbw.on(config.zone) + nodeMilight.commands.rgbw.rgb255(Number('0xAA'), Number('0xAA'), Number('0xAA')) + ] + + describe '#setBrightness', -> + it 'should call the corresponding driver method', -> + device.setBrightness(50) + + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args.should.eql [ + nodeMilight.commands.rgbw.on(config.zone) + nodeMilight.commands.rgbw.brightness(50) + ] + + describe '#changeDimlevelTo', -> + it 'should call the corresponding driver method', -> + device.changeDimlevelTo(50) + + DriverStub.sendCommands.calledOnce.should.equal true + DriverStub.sendCommands.firstCall.args.should.eql [ + nodeMilight.commands.rgbw.on(config.zone) + nodeMilight.commands.rgbw.brightness(50) + ] diff --git a/test/milightRF24.coffee b/test/milightRF24.coffee new file mode 100644 index 0000000..d235725 --- /dev/null +++ b/test/milightRF24.coffee @@ -0,0 +1,150 @@ +should = require 'should' +{ env } = require './app_stub/env' +{ loadPluginWithEnvAndConfig } = require './app_stub/framework' +{ DriverStub } = require './driver_stubs/milightRF24' + +describe 'MilightRF24', -> + device = null + + config = + id: 'some_id' + name: 'some_name' + class: 'MilightRF24' + port: '/dev/ttyUSB1' + zones: [ + { + addr: '5927' + zone: 0 + send: true + receive: true + }, + { + addr: '485D' + zone: 0 + send: true + receive: true + }, + { + addr: '1111' + zone: 0 + send: false + receive: true + } + ] + + beforeEach -> + device = loadPluginWithEnvAndConfig env, 'MilightRF24', config + DriverStub.reset() + + # set default state + device.mode = false + device.color = '' + device.power = 'off' + device.brightness = 100 + + describe '#getPower', -> + it 'should return the current power state (off by default)', (done) -> + device.getPower().then (power) -> + power.should.equal 'off' + done() + + describe '#getMode', -> + it 'should return the current power state (white (false) by default)', (done) -> + device.getMode().then (mode) -> + mode.should.equal false + done() + + describe '#turnOn', -> + it 'should send the corresponding driver commands', -> + device.turnOn() + + DriverStub.sendButton.calledTwice.should.equal true + + DriverStub.sendButton.firstCall.args.should.eql [ '5927', 0, 1, false ] + DriverStub.sendButton.secondCall.args.should.eql [ '485D', 0, 1, false ] + + describe '#turnOff', -> + it 'should send the corresponding driver commands', -> + device.turnOff() + + DriverStub.sendButton.calledTwice.should.equal true + + DriverStub.sendButton.firstCall.args.should.eql [ '5927', 0, 2, false ] + DriverStub.sendButton.secondCall.args.should.eql [ '485D', 0, 2, false ] + + describe '#toggle', -> + it 'should switch the power state to ON when it is OFF before', -> + device.power = 'off' + device.toggle() + + DriverStub.sendButton.calledTwice.should.equal true + + DriverStub.sendButton.firstCall.args.should.eql [ '5927', 0, 1, false ] + DriverStub.sendButton.secondCall.args.should.eql [ '485D', 0, 1, false ] + + it 'should switch the power state to OFF when it is ON before', -> + device.power = 'on' + device.toggle() + + DriverStub.sendButton.calledTwice.should.equal true + + DriverStub.sendButton.firstCall.args.should.eql [ '5927', 0, 2, false ] + DriverStub.sendButton.secondCall.args.should.eql [ '485D', 0, 2, false ] + + describe '#setWhite', -> + it 'should call the corresponding driver method', -> + device.setWhite() + + DriverStub.sendButton.calledTwice.should.equal true + + DriverStub.sendButton.firstCall.args.should.eql [ '5927', 0, 1, true ] + DriverStub.sendButton.secondCall.args.should.eql [ '485D', 0, 1, true ] + + describe '#setColor', -> + it 'should call the corresponding driver method', -> + device.power = 'on' + device.setColor('#AAAAAA') + + DriverStub.setColor.calledTwice.should.equal true + + DriverStub.setColor.firstCall.args.should.eql [ '5927', 0, 170, 170, 170 ] + DriverStub.setColor.secondCall.args.should.eql [ '485D', 0, 170, 170, 170 ] + + context 'device power is "off"', -> + it 'should call the corresponding driver method', -> + device.power = 'off' + device.setColor('#AAAAAA') + + DriverStub.setColor.calledTwice.should.equal true + + DriverStub.setColor.firstCall.args.should.eql [ '5927', 0, 170, 170, 170 ] + DriverStub.setColor.secondCall.args.should.eql [ '485D', 0, 170, 170, 170 ] + + describe '#setBrightness', -> + it 'should call the corresponding driver method', -> + device.power = 'on' + device.setBrightness(50) + + DriverStub.setBrightness.calledTwice.should.equal true + + DriverStub.setBrightness.firstCall.args.should.eql [ '5927', 0, 50 ] + DriverStub.setBrightness.secondCall.args.should.eql [ '485D', 0, 50 ] + + context 'device power is "off"', -> + it 'should call the corresponding driver method', -> + device.power = 'off' + device.setBrightness(50) + + DriverStub.setBrightness.calledTwice.should.equal true + + DriverStub.setBrightness.firstCall.args.should.eql [ '5927', 0, 50 ] + DriverStub.setBrightness.secondCall.args.should.eql [ '485D', 0, 50 ] + + describe '#changeDimlevelTo', -> + it 'should call the corresponding driver method', -> + device.changeDimlevelTo(50) + + DriverStub.setBrightness.calledTwice.should.equal true + + DriverStub.setBrightness.firstCall.args.should.eql [ '5927', 0, 50 ] + DriverStub.setBrightness.secondCall.args.should.eql [ '485D', 0, 50 ] diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..8bd4127 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--compilers coffee:coffee-script/register diff --git a/ui/led-light.coffee b/ui/led-light.coffee index ddefaef..c7bdb6e 100644 --- a/ui/led-light.coffee +++ b/ui/led-light.coffee @@ -7,8 +7,6 @@ $(document).on 'templateinit', (event) -> @id = templData.deviceId - @debounceTimerId = null - @power = null @brightness = null @color = null @@ -52,17 +50,31 @@ $(document).on 'templateinit', (event) -> @brightnessSlider.val(@brightness()).trigger 'change', [origin: 'remote'] @powerSlider.val(@power()).trigger 'change', [origin: 'remote'] - _debounce: (fn, timeout) -> - clearTimeout @debounceTimerId if @debounceTimerId - @debounceTimerId = setTimeout fn, timeout _onLocalChange: (element, fn) -> + timeout = 500 # ms + + # only execute one command at the time + # delay the callback to protect the device against overflow + queue = async.queue (arg, cb) => + fn.call(@, arg) + .done( (data) -> + ajaxShowToast(data) + setTimeout cb, timeout + ) + .fail( (data) -> + ajaxAlertFail(data) + setTimeout cb, timeout + ) + , 1 # concurrency + $('#index').on "change", "#item-lists ##{@id} .light-#{element}", (e, payload) => return if payload?.origin is 'remote' return if @[element]?() is $(e.target).val() - @_debounce => - fn.call @, $(e.target).val() - , 50 + # flush queue to do not pile up commands + # latest command has highest priority + queue.kill() if queue.length() > 2 + queue.push $(e.target).val() _onRemoteChange: (attributeString, el) -> attribute = @getAttribute(attributeString) @@ -76,30 +88,19 @@ $(document).on 'templateinit', (event) -> el.val(@[attributeString]()).trigger 'change', [origin: 'remote'] _setPower: (state) -> - if state is 'on' - @device.rest.turnOn({}, global: no) - .done(ajaxShowToast) - .fail(ajaxAlertFail) + @device.rest.turnOn {}, global: no else - @device.rest.turnOff({}, global: no) - .done(ajaxShowToast) - .fail(ajaxAlertFail) + @device.rest.turnOff {}, global: no _setColor: (colorCode) -> unless colorCode - @device.rest.setWhite({}, global: no) - .done(ajaxShowToast) - .fail(ajaxAlertFail) + @device.rest.setWhite {}, global: no else - @device.rest.setColor({colorCode: colorCode}, global: no) - .done(ajaxShowToast) - .fail(ajaxAlertFail) + @device.rest.setColor colorCode: colorCode, global: no _setBrightness: (brightnessValue) -> - @device.rest.setBrightness({brightnessValue: brightnessValue}, global: no) - .done(ajaxShowToast) - .fail(ajaxAlertFail) + @device.rest.setBrightness {brightnessValue: brightnessValue}, global: no # register the item-class pimatic.templateClasses['led-light'] = LedLightItem diff --git a/ui/vendor/async.js b/ui/vendor/async.js new file mode 100644 index 0000000..b947fc4 --- /dev/null +++ b/ui/vendor/async.js @@ -0,0 +1,2 @@ +!function(){function n(){}function t(n){return n}function e(n){return!!n}function r(n){return!n}function u(n){return function(){if(null===n)throw new Error("Callback was already called.");n.apply(this,arguments),n=null}}function i(n){return function(){null!==n&&(n.apply(this,arguments),n=null)}}function o(n){return M(n)||"number"==typeof n.length&&n.length>=0&&n.length%1===0}function c(n,t){for(var e=-1,r=n.length;++er?r:null}):(e=W(n),t=e.length,function(){return r++,t>r?e[r]:null})}function m(n,t){return t=null==t?n.length-1:+t,function(){for(var e=Math.max(arguments.length-t,0),r=Array(e),u=0;e>u;u++)r[u]=arguments[u+t];switch(t){case 0:return n.call(this,r);case 1:return n.call(this,arguments[0],r)}}}function y(n){return function(t,e,r){return n(t,r)}}function v(t){return function(e,r,o){o=i(o||n),e=e||[];var c=h(e);if(0>=t)return o(null);var a=!1,f=0,l=!1;!function s(){if(a&&0>=f)return o(null);for(;t>f&&!l;){var n=c();if(null===n)return a=!0,void(0>=f&&o(null));f+=1,r(e[n],n,u(function(n){f-=1,n?(o(n),l=!0):s()}))}}()}}function d(n){return function(t,e,r){return n(C.eachOf,t,e,r)}}function g(n){return function(t,e,r,u){return n(v(e),t,r,u)}}function k(n){return function(t,e,r){return n(C.eachOfSeries,t,e,r)}}function b(t,e,r,u){u=i(u||n),e=e||[];var c=o(e)?[]:{};t(e,function(n,t,e){r(n,function(n,r){c[t]=r,e(n)})},function(n){u(n,c)})}function w(n,t,e,r){var u=[];n(t,function(n,t,r){e(n,function(e){e&&u.push({index:t,value:n}),r()})},function(){r(a(u.sort(function(n,t){return n.index-t.index}),function(n){return n.value}))})}function O(n,t,e,r){w(n,t,function(n,t){e(n,function(n){t(!n)})},r)}function S(n,t,e){return function(r,u,i,o){function c(){o&&o(e(!1,void 0))}function a(n,r,u){return o?void i(n,function(r){o&&t(r)&&(o(e(!0,n)),o=i=!1),u()}):u()}arguments.length>3?n(r,u,a,c):(o=i,i=u,n(r,a,c))}}function E(n,t){return t}function L(t,e,r){r=r||n;var u=o(e)?[]:{};t(e,function(n,t,e){n(m(function(n,r){r.length<=1&&(r=r[0]),u[t]=r,e(n)}))},function(n){r(n,u)})}function I(n,t,e,r){var u=[];n(t,function(n,t,r){e(n,function(n,t){u=u.concat(t||[]),r(n)})},function(n){r(n,u)})}function x(t,e,r){function i(t,e,r,u){if(null!=u&&"function"!=typeof u)throw new Error("task callback must be a function");return t.started=!0,M(e)||(e=[e]),0===e.length&&t.idle()?C.setImmediate(function(){t.drain()}):(c(e,function(e){var i={data:e,callback:u||n};r?t.tasks.unshift(i):t.tasks.push(i),t.tasks.length===t.concurrency&&t.saturated()}),void C.setImmediate(t.process))}function o(n,t){return function(){f-=1;var e=!1,r=arguments;c(t,function(n){c(l,function(t,r){t!==n||e||(l.splice(r,1),e=!0)}),n.callback.apply(n,r)}),n.tasks.length+f===0&&n.drain(),n.process()}}if(null==e)e=1;else if(0===e)throw new Error("Concurrency must not be zero");var f=0,l=[],s={tasks:[],concurrency:e,payload:r,saturated:n,empty:n,drain:n,started:!1,paused:!1,push:function(n,t){i(s,n,!1,t)},kill:function(){s.drain=n,s.tasks=[]},unshift:function(n,t){i(s,n,!0,t)},process:function(){if(!s.paused&&f=t;t++)C.setImmediate(s.process)}}};return s}function j(n){return m(function(t,e){t.apply(null,e.concat([m(function(t,e){"object"==typeof console&&(t?console.error&&console.error(t):console[n]&&c(e,function(t){console[n](t)}))})]))})}function A(n){return function(t,e,r){n(f(t),e,r)}}function T(n){return m(function(t,e){var r=m(function(e){var r=this,u=e.pop();return n(t,function(n,t,u){n.apply(r,e.concat([u]))},u)});return e.length?r.apply(this,e):r})}function z(n){return m(function(t){var e=t.pop();t.push(function(){var n=arguments;r?C.setImmediate(function(){e.apply(null,n)}):e.apply(null,n)});var r=!0;n.apply(this,t),r=!1})}var q,C={},P="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this;null!=P&&(q=P.async),C.noConflict=function(){return P.async=q,C};var H=Object.prototype.toString,M=Array.isArray||function(n){return"[object Array]"===H.call(n)},U=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},W=Object.keys||function(n){var t=[];for(var e in n)n.hasOwnProperty(e)&&t.push(e);return t},B="function"==typeof setImmediate&&setImmediate,D=B?function(n){B(n)}:function(n){setTimeout(n,0)};"object"==typeof process&&"function"==typeof process.nextTick?C.nextTick=process.nextTick:C.nextTick=D,C.setImmediate=B?D:C.nextTick,C.forEach=C.each=function(n,t,e){return C.eachOf(n,y(t),e)},C.forEachSeries=C.eachSeries=function(n,t,e){return C.eachOfSeries(n,y(t),e)},C.forEachLimit=C.eachLimit=function(n,t,e,r){return v(t)(n,y(e),r)},C.forEachOf=C.eachOf=function(t,e,r){function o(n){f--,n?r(n):null===c&&0>=f&&r(null)}r=i(r||n),t=t||[];for(var c,a=h(t),f=0;null!=(c=a());)f+=1,e(t[c],c,u(o));0===f&&r(null)},C.forEachOfSeries=C.eachOfSeries=function(t,e,r){function o(){var n=!0;return null===a?r(null):(e(t[a],a,u(function(t){if(t)r(t);else{if(a=c(),null===a)return r(null);n?C.setImmediate(o):o()}})),void(n=!1))}r=i(r||n),t=t||[];var c=h(t),a=c();o()},C.forEachOfLimit=C.eachOfLimit=function(n,t,e,r){v(t)(n,e,r)},C.map=d(b),C.mapSeries=k(b),C.mapLimit=g(b),C.inject=C.foldl=C.reduce=function(n,t,e,r){C.eachOfSeries(n,function(n,r,u){e(t,n,function(n,e){t=e,u(n)})},function(n){r(n,t)})},C.foldr=C.reduceRight=function(n,e,r,u){var i=a(n,t).reverse();C.reduce(i,e,r,u)},C.transform=function(n,t,e,r){3===arguments.length&&(r=e,e=t,t=M(n)?[]:{}),C.eachOf(n,function(n,r,u){e(t,n,r,u)},function(n){r(n,t)})},C.select=C.filter=d(w),C.selectLimit=C.filterLimit=g(w),C.selectSeries=C.filterSeries=k(w),C.reject=d(O),C.rejectLimit=g(O),C.rejectSeries=k(O),C.any=C.some=S(C.eachOf,e,t),C.someLimit=S(C.eachOfLimit,e,t),C.all=C.every=S(C.eachOf,r,r),C.everyLimit=S(C.eachOfLimit,r,r),C.detect=S(C.eachOf,t,E),C.detectSeries=S(C.eachOfSeries,t,E),C.detectLimit=S(C.eachOfLimit,t,E),C.sortBy=function(n,t,e){function r(n,t){var e=n.criteria,r=t.criteria;return r>e?-1:e>r?1:0}C.map(n,function(n,e){t(n,function(t,r){t?e(t):e(null,{value:n,criteria:r})})},function(n,t){return n?e(n):void e(null,a(t.sort(r),function(n){return n.value}))})},C.auto=function(t,e,r){function u(n){d.unshift(n)}function o(n){var t=p(d,n);t>=0&&d.splice(t,1)}function a(){h--,c(d.slice(0),function(n){n()})}r||(r=e,e=null),r=i(r||n);var f=W(t),h=f.length;if(!h)return r(null);e||(e=h);var y={},v=0,d=[];u(function(){h||r(null,y)}),c(f,function(n){function i(){return e>v&&l(g,function(n,t){return n&&y.hasOwnProperty(t)},!0)&&!y.hasOwnProperty(n)}function c(){i()&&(v++,o(c),h[h.length-1](d,y))}for(var f,h=M(t[n])?t[n]:[t[n]],d=m(function(t,e){if(v--,e.length<=1&&(e=e[0]),t){var u={};s(y,function(n,t){u[t]=n}),u[n]=e,r(t,u)}else y[n]=e,C.setImmediate(a)}),g=h.slice(0,h.length-1),k=g.length;k--;){if(!(f=t[g[k]]))throw new Error("Has inexistant dependency");if(M(f)&&p(f,n)>=0)throw new Error("Has cyclic dependencies")}i()?(v++,h[h.length-1](d,y)):u(c)})},C.retry=function(n,t,e){function r(n,t){if("number"==typeof t)n.times=parseInt(t,10)||i;else{if("object"!=typeof t)throw new Error("Unsupported argument type for 'times': "+typeof t);n.times=parseInt(t.times,10)||i,n.interval=parseInt(t.interval,10)||o}}function u(n,t){function e(n,e){return function(r){n(function(n,t){r(!n||e,{err:n,result:t})},t)}}function r(n){return function(t){setTimeout(function(){t(null)},n)}}for(;a.times;){var u=!(a.times-=1);c.push(e(a.task,u)),!u&&a.interval>0&&c.push(r(a.interval))}C.series(c,function(t,e){e=e[e.length-1],(n||a.callback)(e.err,e.result)})}var i=5,o=0,c=[],a={times:i,interval:o},f=arguments.length;if(1>f||f>3)throw new Error("Invalid arguments - must be either (task), (task, callback), (times, task) or (times, task, callback)");return 2>=f&&"function"==typeof n&&(e=t,t=n),"function"!=typeof n&&r(a,n),a.callback=e,a.task=t,a.callback?u():u},C.waterfall=function(t,e){function r(n){return m(function(t,u){if(t)e.apply(null,[t].concat(u));else{var i=n.next();i?u.push(r(i)):u.push(e),z(n).apply(null,u)}})}if(e=i(e||n),!M(t)){var u=new Error("First argument to waterfall must be an array of functions");return e(u)}return t.length?void r(C.iterator(t))():e()},C.parallel=function(n,t){L(C.eachOf,n,t)},C.parallelLimit=function(n,t,e){L(v(t),n,e)},C.series=function(n,t){L(C.eachOfSeries,n,t)},C.iterator=function(n){function t(e){function r(){return n.length&&n[e].apply(null,arguments),r.next()}return r.next=function(){return er;){var i=r+(u-r+1>>>1);e(t,n[i])>=0?r=i:u=i-1}return r}function i(t,e,i,o){if(null!=o&&"function"!=typeof o)throw new Error("task callback must be a function");return t.started=!0,M(e)||(e=[e]),0===e.length?C.setImmediate(function(){t.drain()}):void c(e,function(e){var c={data:e,priority:i,callback:"function"==typeof o?o:n};t.tasks.splice(u(t.tasks,c,r)+1,0,c),t.tasks.length===t.concurrency&&t.saturated(),C.setImmediate(t.process)})}var o=C.queue(t,e);return o.push=function(n,t,e){i(o,n,t,e)},delete o.unshift,o},C.cargo=function(n,t){return x(n,1,t)},C.log=j("log"),C.dir=j("dir"),C.memoize=function(n,e){var r={},u={};e=e||t;var i=m(function(t){var i=t.pop(),o=e.apply(null,t);o in r?C.setImmediate(function(){i.apply(null,r[o])}):o in u?u[o].push(i):(u[o]=[i],n.apply(null,t.concat([m(function(n){r[o]=n;var t=u[o];delete u[o];for(var e=0,i=t.length;i>e;e++)t[e].apply(null,n)})])))});return i.memo=r,i.unmemoized=n,i},C.unmemoize=function(n){return function(){return(n.unmemoized||n).apply(null,arguments)}},C.times=A(C.map),C.timesSeries=A(C.mapSeries),C.timesLimit=function(n,t,e,r){return C.mapLimit(f(n),t,e,r)},C.seq=function(){var t=arguments;return m(function(e){var r=this,u=e[e.length-1];"function"==typeof u?e.pop():u=n,C.reduce(t,e,function(n,t,e){t.apply(r,n.concat([m(function(n,t){e(n,t)})]))},function(n,t){u.apply(r,[n].concat(t))})})},C.compose=function(){return C.seq.apply(null,Array.prototype.reverse.call(arguments))},C.applyEach=T(C.eachOf),C.applyEachSeries=T(C.eachOfSeries),C.forever=function(t,e){function r(n){return n?i(n):void o(r)}var i=u(e||n),o=z(t);r()},C.ensureAsync=z,C.constant=m(function(n){var t=[null].concat(n);return function(n){return n.apply(this,t)}}),C.wrapSync=C.asyncify=function(n){return m(function(t){var e,r=t.pop();try{e=n.apply(this,t)}catch(u){return r(u)}U(e)&&"function"==typeof e.then?e.then(function(n){r(null,n)})["catch"](function(n){r(n.message?n:new Error(n))}):r(null,e)})},"object"==typeof module&&module.exports?module.exports=C:"function"==typeof define&&define.amd?define([],function(){return C}):P.async=C}(); +//# sourceMappingURL=dist/async.min.map \ No newline at end of file diff --git a/ui/vendor/spectrum.css b/ui/vendor/spectrum.css old mode 100755 new mode 100644 index 2846613..a8ad9e4 --- a/ui/vendor/spectrum.css +++ b/ui/vendor/spectrum.css @@ -1,5 +1,5 @@ /*** -Spectrum Colorpicker v1.6.0 +Spectrum Colorpicker v1.8.0 https://github.com/bgrins/spectrum Author: Brian Grinstead License: MIT diff --git a/ui/vendor/spectrum.js b/ui/vendor/spectrum.js old mode 100755 new mode 100644 index 5667329..88063d0 --- a/ui/vendor/spectrum.js +++ b/ui/vendor/spectrum.js @@ -1,2287 +1 @@ -// Spectrum Colorpicker v1.6.0 -// https://github.com/bgrins/spectrum -// Author: Brian Grinstead -// License: MIT - -(function (factory) { - "use strict"; - - if (typeof define === 'function' && define.amd) { // AMD - define(['jquery'], factory); - } - else if (typeof exports == "object" && typeof module == "object") { // CommonJS - module.exports = factory; - } - else { // Browser - factory(jQuery); - } -})(function($, undefined) { - "use strict"; - - var defaultOpts = { - - // Callbacks - beforeShow: noop, - move: noop, - change: noop, - show: noop, - hide: noop, - - // Options - color: false, - flat: false, - showInput: false, - allowEmpty: false, - showButtons: true, - clickoutFiresChange: false, - showInitial: false, - showPalette: false, - showPaletteOnly: false, - hideAfterPaletteSelect: false, - togglePaletteOnly: false, - showSelectionPalette: true, - localStorageKey: false, - appendTo: "body", - maxSelectionSize: 7, - cancelText: "cancel", - chooseText: "choose", - togglePaletteMoreText: "more", - togglePaletteLessText: "less", - clearText: "Clear Color Selection", - noColorSelectedText: "No Color Selected", - preferredFormat: false, - className: "", // Deprecated - use containerClassName and replacerClassName instead. - containerClassName: "", - replacerClassName: "", - showAlpha: false, - theme: "sp-light", - palette: [["#ffffff", "#000000", "#ff0000", "#ff8000", "#ffff00", "#008000", "#0000ff", "#4b0082", "#9400d3"]], - selectionPalette: [], - disabled: false, - offset: null - }, - spectrums = [], - IE = !!/msie/i.exec( window.navigator.userAgent ), - rgbaSupport = (function() { - function contains( str, substr ) { - return !!~('' + str).indexOf(substr); - } - - var elem = document.createElement('div'); - var style = elem.style; - style.cssText = 'background-color:rgba(0,0,0,.5)'; - return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla'); - })(), - inputTypeColorSupport = (function() { - var colorInput = $("")[0]; - return colorInput.type === "color" && colorInput.value !== "!"; - })(), - replaceInput = [ - "
", - "
", - "
", - "
" - ].join(''), - markup = (function () { - - // IE does not support gradients with multiple stops, so we need to simulate - // that for the rainbow slider with 8 divs that each have a single gradient - var gradientFix = ""; - if (IE) { - for (var i = 1; i <= 6; i++) { - gradientFix += "
"; - } - } - - return [ - "
", - "
", - "
", - "
", - "", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - "
", - gradientFix, - "
", - "
", - "
", - "
", - "
", - "", - "
", - "
", - "
", - "", - "", - "
", - "
", - "
" - ].join(""); - })(); - - function paletteTemplate (p, color, className, opts) { - var html = []; - for (var i = 0; i < p.length; i++) { - var current = p[i]; - if(current) { - var tiny = tinycolor(current); - var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light"; - c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : ""; - var formattedString = tiny.toString(opts.preferredFormat || "rgb"); - var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter(); - html.push(''); - } else { - var cls = 'sp-clear-display'; - html.push($('
') - .append($('') - .attr('title', opts.noColorSelectedText) - ) - .html() - ); - } - } - return "
" + html.join('') + "
"; - } - - function hideAll() { - for (var i = 0; i < spectrums.length; i++) { - if (spectrums[i]) { - spectrums[i].hide(); - } - } - } - - function instanceOptions(o, callbackContext) { - var opts = $.extend({}, defaultOpts, o); - opts.callbacks = { - 'move': bind(opts.move, callbackContext), - 'change': bind(opts.change, callbackContext), - 'show': bind(opts.show, callbackContext), - 'hide': bind(opts.hide, callbackContext), - 'beforeShow': bind(opts.beforeShow, callbackContext) - }; - - return opts; - } - - function spectrum(element, o) { - - var opts = instanceOptions(o, element), - flat = opts.flat, - showSelectionPalette = opts.showSelectionPalette, - localStorageKey = opts.localStorageKey, - theme = opts.theme, - callbacks = opts.callbacks, - resize = throttle(reflow, 10), - visible = false, - dragWidth = 0, - dragHeight = 0, - dragHelperHeight = 0, - slideHeight = 0, - slideWidth = 0, - alphaWidth = 0, - alphaSlideHelperWidth = 0, - slideHelperHeight = 0, - currentHue = 0, - currentSaturation = 0, - currentValue = 0, - currentAlpha = 1, - palette = [], - paletteArray = [], - paletteLookup = {}, - selectionPalette = opts.selectionPalette.slice(0), - maxSelectionSize = opts.maxSelectionSize, - draggingClass = "sp-dragging", - shiftMovementDirection = null; - - var doc = element.ownerDocument, - body = doc.body, - boundElement = $(element), - disabled = false, - container = $(markup, doc).addClass(theme), - pickerContainer = container.find(".sp-picker-container"), - dragger = container.find(".sp-color"), - dragHelper = container.find(".sp-dragger"), - slider = container.find(".sp-hue"), - slideHelper = container.find(".sp-slider"), - alphaSliderInner = container.find(".sp-alpha-inner"), - alphaSlider = container.find(".sp-alpha"), - alphaSlideHelper = container.find(".sp-alpha-handle"), - textInput = container.find(".sp-input"), - paletteContainer = container.find(".sp-palette"), - initialColorContainer = container.find(".sp-initial"), - cancelButton = container.find(".sp-cancel"), - clearButton = container.find(".sp-clear"), - chooseButton = container.find(".sp-choose"), - toggleButton = container.find(".sp-palette-toggle"), - isInput = boundElement.is("input"), - isInputTypeColor = isInput && inputTypeColorSupport && boundElement.attr("type") === "color", - shouldReplace = isInput && !flat, - replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className).addClass(opts.replacerClassName) : $([]), - offsetElement = (shouldReplace) ? replacer : boundElement, - previewElement = replacer.find(".sp-preview-inner"), - initialColor = opts.color || (isInput && boundElement.val()), - colorOnShow = false, - preferredFormat = opts.preferredFormat, - currentPreferredFormat = preferredFormat, - clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange, - isEmpty = !initialColor, - allowEmpty = opts.allowEmpty && !isInputTypeColor; - - function applyOptions() { - - if (opts.showPaletteOnly) { - opts.showPalette = true; - } - - toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText); - - if (opts.palette) { - palette = opts.palette.slice(0); - paletteArray = $.isArray(palette[0]) ? palette : [palette]; - paletteLookup = {}; - for (var i = 0; i < paletteArray.length; i++) { - for (var j = 0; j < paletteArray[i].length; j++) { - var rgb = tinycolor(paletteArray[i][j]).toRgbString(); - paletteLookup[rgb] = true; - } - } - } - - container.toggleClass("sp-flat", flat); - container.toggleClass("sp-input-disabled", !opts.showInput); - container.toggleClass("sp-alpha-enabled", opts.showAlpha); - container.toggleClass("sp-clear-enabled", allowEmpty); - container.toggleClass("sp-buttons-disabled", !opts.showButtons); - container.toggleClass("sp-palette-buttons-disabled", !opts.togglePaletteOnly); - container.toggleClass("sp-palette-disabled", !opts.showPalette); - container.toggleClass("sp-palette-only", opts.showPaletteOnly); - container.toggleClass("sp-initial-disabled", !opts.showInitial); - container.addClass(opts.className).addClass(opts.containerClassName); - - reflow(); - } - - function initialize() { - - if (IE) { - container.find("*:not(input)").attr("unselectable", "on"); - } - - applyOptions(); - - if (shouldReplace) { - boundElement.after(replacer).hide(); - } - - if (!allowEmpty) { - clearButton.hide(); - } - - if (flat) { - boundElement.after(container).hide(); - } - else { - - var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo); - if (appendTo.length !== 1) { - appendTo = $("body"); - } - - appendTo.append(container); - } - - updateSelectionPaletteFromStorage(); - - offsetElement.bind("click.spectrum touchstart.spectrum", function (e) { - if (!disabled) { - toggle(); - } - - e.stopPropagation(); - - if (!$(e.target).is("input")) { - e.preventDefault(); - } - }); - - if(boundElement.is(":disabled") || (opts.disabled === true)) { - disable(); - } - - // Prevent clicks from bubbling up to document. This would cause it to be hidden. - container.click(stopPropagation); - - // Handle user typed input - textInput.change(setFromTextInput); - textInput.bind("paste", function () { - setTimeout(setFromTextInput, 1); - }); - textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } }); - - cancelButton.text(opts.cancelText); - cancelButton.bind("click.spectrum", function (e) { - e.stopPropagation(); - e.preventDefault(); - revert(); - hide(); - }); - - clearButton.attr("title", opts.clearText); - clearButton.bind("click.spectrum", function (e) { - e.stopPropagation(); - e.preventDefault(); - isEmpty = true; - move(); - - if(flat) { - //for the flat style, this is a change event - updateOriginalInput(true); - } - }); - - chooseButton.text(opts.chooseText); - chooseButton.bind("click.spectrum", function (e) { - e.stopPropagation(); - e.preventDefault(); - - if (isValid()) { - updateOriginalInput(true); - hide(); - } - }); - - toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText); - toggleButton.bind("click.spectrum", function (e) { - e.stopPropagation(); - e.preventDefault(); - - opts.showPaletteOnly = !opts.showPaletteOnly; - - // To make sure the Picker area is drawn on the right, next to the - // Palette area (and not below the palette), first move the Palette - // to the left to make space for the picker, plus 5px extra. - // The 'applyOptions' function puts the whole container back into place - // and takes care of the button-text and the sp-palette-only CSS class. - if (!opts.showPaletteOnly && !flat) { - container.css('left', '-=' + (pickerContainer.outerWidth(true) + 5)); - } - applyOptions(); - }); - - draggable(alphaSlider, function (dragX, dragY, e) { - currentAlpha = (dragX / alphaWidth); - isEmpty = false; - if (e.shiftKey) { - currentAlpha = Math.round(currentAlpha * 10) / 10; - } - - move(); - }, dragStart, dragStop); - - draggable(slider, function (dragX, dragY) { - currentHue = parseFloat(dragY / slideHeight); - isEmpty = false; - if (!opts.showAlpha) { - currentAlpha = 1; - } - move(); - }, dragStart, dragStop); - - draggable(dragger, function (dragX, dragY, e) { - - // shift+drag should snap the movement to either the x or y axis. - if (!e.shiftKey) { - shiftMovementDirection = null; - } - else if (!shiftMovementDirection) { - var oldDragX = currentSaturation * dragWidth; - var oldDragY = dragHeight - (currentValue * dragHeight); - var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY); - - shiftMovementDirection = furtherFromX ? "x" : "y"; - } - - var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x"; - var setValue = !shiftMovementDirection || shiftMovementDirection === "y"; - - if (setSaturation) { - currentSaturation = parseFloat(dragX / dragWidth); - } - if (setValue) { - currentValue = parseFloat((dragHeight - dragY) / dragHeight); - } - - isEmpty = false; - if (!opts.showAlpha) { - currentAlpha = 1; - } - - move(); - - }, dragStart, dragStop); - - if (!!initialColor) { - set(initialColor); - - // In case color was black - update the preview UI and set the format - // since the set function will not run (default color is black). - updateUI(); - currentPreferredFormat = preferredFormat || tinycolor(initialColor).format; - - addColorToSelectionPalette(initialColor); - } - else { - updateUI(); - } - - if (flat) { - show(); - } - - function paletteElementClick(e) { - if (e.data && e.data.ignore) { - set($(e.target).closest(".sp-thumb-el").data("color")); - move(); - } - else { - set($(e.target).closest(".sp-thumb-el").data("color")); - move(); - updateOriginalInput(true); - if (opts.hideAfterPaletteSelect) { - hide(); - } - } - - return false; - } - - var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum"; - paletteContainer.delegate(".sp-thumb-el", paletteEvent, paletteElementClick); - initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, paletteElementClick); - } - - function updateSelectionPaletteFromStorage() { - - if (localStorageKey && window.localStorage) { - - // Migrate old palettes over to new format. May want to remove this eventually. - try { - var oldPalette = window.localStorage[localStorageKey].split(",#"); - if (oldPalette.length > 1) { - delete window.localStorage[localStorageKey]; - $.each(oldPalette, function(i, c) { - addColorToSelectionPalette(c); - }); - } - } - catch(e) { } - - try { - selectionPalette = window.localStorage[localStorageKey].split(";"); - } - catch (e) { } - } - } - - function addColorToSelectionPalette(color) { - if (showSelectionPalette) { - var rgb = tinycolor(color).toRgbString(); - if (!paletteLookup[rgb] && $.inArray(rgb, selectionPalette) === -1) { - selectionPalette.push(rgb); - while(selectionPalette.length > maxSelectionSize) { - selectionPalette.shift(); - } - } - - if (localStorageKey && window.localStorage) { - try { - window.localStorage[localStorageKey] = selectionPalette.join(";"); - } - catch(e) { } - } - } - } - - function getUniqueSelectionPalette() { - var unique = []; - if (opts.showPalette) { - for (var i = 0; i < selectionPalette.length; i++) { - var rgb = tinycolor(selectionPalette[i]).toRgbString(); - - if (!paletteLookup[rgb]) { - unique.push(selectionPalette[i]); - } - } - } - - return unique.reverse().slice(0, opts.maxSelectionSize); - } - - function drawPalette() { - - var currentColor = get(); - - var html = $.map(paletteArray, function (palette, i) { - return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i, opts); - }); - - updateSelectionPaletteFromStorage(); - - if (selectionPalette) { - html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection", opts)); - } - - paletteContainer.html(html.join("")); - } - - function drawInitial() { - if (opts.showInitial) { - var initial = colorOnShow; - var current = get(); - initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial", opts)); - } - } - - function dragStart() { - if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) { - reflow(); - } - container.addClass(draggingClass); - shiftMovementDirection = null; - boundElement.trigger('dragstart.spectrum', [ get() ]); - } - - function dragStop() { - container.removeClass(draggingClass); - boundElement.trigger('dragstop.spectrum', [ get() ]); - } - - function setFromTextInput() { - - var value = textInput.val(); - - if ((value === null || value === "") && allowEmpty) { - set(null); - updateOriginalInput(true); - } - else { - var tiny = tinycolor(value); - if (tiny.isValid()) { - set(tiny); - updateOriginalInput(true); - } - else { - textInput.addClass("sp-validation-error"); - } - } - } - - function toggle() { - if (visible) { - hide(); - } - else { - show(); - } - } - - function show() { - var event = $.Event('beforeShow.spectrum'); - - if (visible) { - reflow(); - return; - } - - boundElement.trigger(event, [ get() ]); - - if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) { - return; - } - - hideAll(); - visible = true; - - $(doc).bind("click.spectrum", clickout); - $(window).bind("resize.spectrum", resize); - replacer.addClass("sp-active"); - container.removeClass("sp-hidden"); - - reflow(); - updateUI(); - - colorOnShow = get(); - - drawInitial(); - callbacks.show(colorOnShow); - boundElement.trigger('show.spectrum', [ colorOnShow ]); - } - - function clickout(e) { - // Return on right click. - if (e.button == 2) { return; } - - if (clickoutFiresChange) { - updateOriginalInput(true); - } - else { - revert(); - } - hide(); - } - - function hide() { - // Return if hiding is unnecessary - if (!visible || flat) { return; } - visible = false; - - $(doc).unbind("click.spectrum", clickout); - $(window).unbind("resize.spectrum", resize); - - replacer.removeClass("sp-active"); - container.addClass("sp-hidden"); - - callbacks.hide(get()); - boundElement.trigger('hide.spectrum', [ get() ]); - } - - function revert() { - set(colorOnShow, true); - } - - function set(color, ignoreFormatChange) { - if (tinycolor.equals(color, get())) { - // Update UI just in case a validation error needs - // to be cleared. - updateUI(); - return; - } - - var newColor, newHsv; - if (!color && allowEmpty) { - isEmpty = true; - } else { - isEmpty = false; - newColor = tinycolor(color); - newHsv = newColor.toHsv(); - - currentHue = (newHsv.h % 360) / 360; - currentSaturation = newHsv.s; - currentValue = newHsv.v; - currentAlpha = newHsv.a; - } - updateUI(); - - if (newColor && newColor.isValid() && !ignoreFormatChange) { - currentPreferredFormat = preferredFormat || newColor.getFormat(); - } - } - - function get(opts) { - opts = opts || { }; - - if (allowEmpty && isEmpty) { - return null; - } - - return tinycolor.fromRatio({ - h: currentHue, - s: currentSaturation, - v: currentValue, - a: Math.round(currentAlpha * 100) / 100 - }, { format: opts.format || currentPreferredFormat }); - } - - function isValid() { - return !textInput.hasClass("sp-validation-error"); - } - - function move() { - updateUI(); - - callbacks.move(get()); - boundElement.trigger('move.spectrum', [ get() ]); - } - - function updateUI() { - - textInput.removeClass("sp-validation-error"); - - updateHelperLocations(); - - // Update dragger background color (gradients take care of saturation and value). - var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 }); - dragger.css("background-color", flatColor.toHexString()); - - // Get a format that alpha will be included in (hex and names ignore alpha) - var format = currentPreferredFormat; - if (currentAlpha < 1 && !(currentAlpha === 0 && format === "name")) { - if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") { - format = "rgb"; - } - } - - var realColor = get({ format: format }), - displayColor = ''; - - //reset background info for preview element - previewElement.removeClass("sp-clear-display"); - previewElement.css('background-color', 'transparent'); - - if (!realColor && allowEmpty) { - // Update the replaced elements background with icon indicating no color selection - previewElement.addClass("sp-clear-display"); - } - else { - var realHex = realColor.toHexString(), - realRgb = realColor.toRgbString(); - - // Update the replaced elements background color (with actual selected color) - if (rgbaSupport || realColor.alpha === 1) { - previewElement.css("background-color", realRgb); - } - else { - previewElement.css("background-color", "transparent"); - previewElement.css("filter", realColor.toFilter()); - } - - if (opts.showAlpha) { - var rgb = realColor.toRgb(); - rgb.a = 0; - var realAlpha = tinycolor(rgb).toRgbString(); - var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")"; - - if (IE) { - alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex)); - } - else { - alphaSliderInner.css("background", "-webkit-" + gradient); - alphaSliderInner.css("background", "-moz-" + gradient); - alphaSliderInner.css("background", "-ms-" + gradient); - // Use current syntax gradient on unprefixed property. - alphaSliderInner.css("background", - "linear-gradient(to right, " + realAlpha + ", " + realHex + ")"); - } - } - - displayColor = realColor.toString(format); - } - - // Update the text entry input as it changes happen - if (opts.showInput) { - textInput.val(displayColor); - } - - if (opts.showPalette) { - drawPalette(); - } - - drawInitial(); - } - - function updateHelperLocations() { - var s = currentSaturation; - var v = currentValue; - - if(allowEmpty && isEmpty) { - //if selected color is empty, hide the helpers - alphaSlideHelper.hide(); - slideHelper.hide(); - dragHelper.hide(); - } - else { - //make sure helpers are visible - alphaSlideHelper.show(); - slideHelper.show(); - dragHelper.show(); - - // Where to show the little circle in that displays your current selected color - var dragX = s * dragWidth; - var dragY = dragHeight - (v * dragHeight); - dragX = Math.max( - -dragHelperHeight, - Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight) - ); - dragY = Math.max( - -dragHelperHeight, - Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight) - ); - dragHelper.css({ - "top": dragY + "px", - "left": dragX + "px" - }); - - var alphaX = currentAlpha * alphaWidth; - alphaSlideHelper.css({ - "left": (alphaX - (alphaSlideHelperWidth / 2)) + "px" - }); - - // Where to show the bar that displays your current selected hue - var slideY = (currentHue) * slideHeight; - slideHelper.css({ - "top": (slideY - slideHelperHeight) + "px" - }); - } - } - - function updateOriginalInput(fireCallback) { - var color = get(), - displayColor = '', - hasChanged = !tinycolor.equals(color, colorOnShow); - - if (color) { - displayColor = color.toString(currentPreferredFormat); - // Update the selection palette with the current color - addColorToSelectionPalette(color); - } - - if (isInput) { - boundElement.val(displayColor); - } - - if (fireCallback && hasChanged) { - callbacks.change(color); - boundElement.trigger('change', [ color ]); - } - } - - function reflow() { - dragWidth = dragger.width(); - dragHeight = dragger.height(); - dragHelperHeight = dragHelper.height(); - slideWidth = slider.width(); - slideHeight = slider.height(); - slideHelperHeight = slideHelper.height(); - alphaWidth = alphaSlider.width(); - alphaSlideHelperWidth = alphaSlideHelper.width(); - - if (!flat) { - container.css("position", "absolute"); - if (opts.offset) { - container.offset(opts.offset); - } else { - container.offset(getOffset(container, offsetElement)); - } - } - - updateHelperLocations(); - - if (opts.showPalette) { - drawPalette(); - } - - boundElement.trigger('reflow.spectrum'); - } - - function destroy() { - boundElement.show(); - offsetElement.unbind("click.spectrum touchstart.spectrum"); - container.remove(); - replacer.remove(); - spectrums[spect.id] = null; - } - - function option(optionName, optionValue) { - if (optionName === undefined) { - return $.extend({}, opts); - } - if (optionValue === undefined) { - return opts[optionName]; - } - - opts[optionName] = optionValue; - applyOptions(); - } - - function enable() { - disabled = false; - boundElement.attr("disabled", false); - offsetElement.removeClass("sp-disabled"); - } - - function disable() { - hide(); - disabled = true; - boundElement.attr("disabled", true); - offsetElement.addClass("sp-disabled"); - } - - function setOffset(coord) { - opts.offset = coord; - reflow(); - } - - initialize(); - - var spect = { - show: show, - hide: hide, - toggle: toggle, - reflow: reflow, - option: option, - enable: enable, - disable: disable, - offset: setOffset, - set: function (c) { - set(c); - updateOriginalInput(); - }, - get: get, - destroy: destroy, - container: container - }; - - spect.id = spectrums.push(spect) - 1; - - return spect; - } - - /** - * checkOffset - get the offset below/above and left/right element depending on screen position - * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js - */ - function getOffset(picker, input) { - var extraY = 0; - var dpWidth = picker.outerWidth(); - var dpHeight = picker.outerHeight(); - var inputHeight = input.outerHeight(); - var doc = picker[0].ownerDocument; - var docElem = doc.documentElement; - var viewWidth = docElem.clientWidth + $(doc).scrollLeft(); - var viewHeight = docElem.clientHeight + $(doc).scrollTop(); - var offset = input.offset(); - offset.top += inputHeight; - - offset.left -= - Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? - Math.abs(offset.left + dpWidth - viewWidth) : 0); - - offset.top -= - Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? - Math.abs(dpHeight + inputHeight - extraY) : extraY)); - - return offset; - } - - /** - * noop - do nothing - */ - function noop() { - - } - - /** - * stopPropagation - makes the code only doing this a little easier to read in line - */ - function stopPropagation(e) { - e.stopPropagation(); - } - - /** - * Create a function bound to a given object - * Thanks to underscore.js - */ - function bind(func, obj) { - var slice = Array.prototype.slice; - var args = slice.call(arguments, 2); - return function () { - return func.apply(obj, args.concat(slice.call(arguments))); - }; - } - - /** - * Lightweight drag helper. Handles containment within the element, so that - * when dragging, the x is within [0,element.width] and y is within [0,element.height] - */ - function draggable(element, onmove, onstart, onstop) { - onmove = onmove || function () { }; - onstart = onstart || function () { }; - onstop = onstop || function () { }; - var doc = document; - var dragging = false; - var offset = {}; - var maxHeight = 0; - var maxWidth = 0; - var hasTouch = ('ontouchstart' in window); - - var duringDragEvents = {}; - duringDragEvents["selectstart"] = prevent; - duringDragEvents["dragstart"] = prevent; - duringDragEvents["touchmove mousemove"] = move; - duringDragEvents["touchend mouseup"] = stop; - - function prevent(e) { - if (e.stopPropagation) { - e.stopPropagation(); - } - if (e.preventDefault) { - e.preventDefault(); - } - e.returnValue = false; - } - - function move(e) { - if (dragging) { - // Mouseup happened outside of window - if (IE && doc.documentMode < 9 && !e.button) { - return stop(); - } - - var touches = e.originalEvent && e.originalEvent.touches; - var pageX = touches ? touches[0].pageX : e.pageX; - var pageY = touches ? touches[0].pageY : e.pageY; - - var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); - var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); - - if (hasTouch) { - // Stop scrolling in iOS - prevent(e); - } - - onmove.apply(element, [dragX, dragY, e]); - } - } - - function start(e) { - var rightclick = (e.which) ? (e.which == 3) : (e.button == 2); - - if (!rightclick && !dragging) { - if (onstart.apply(element, arguments) !== false) { - dragging = true; - maxHeight = $(element).height(); - maxWidth = $(element).width(); - offset = $(element).offset(); - - $(doc).bind(duringDragEvents); - $(doc.body).addClass("sp-dragging"); - - if (!hasTouch) { - move(e); - } - - prevent(e); - } - } - } - - function stop() { - if (dragging) { - $(doc).unbind(duringDragEvents); - $(doc.body).removeClass("sp-dragging"); - onstop.apply(element, arguments); - } - dragging = false; - } - - $(element).bind("touchstart mousedown", start); - } - - function throttle(func, wait, debounce) { - var timeout; - return function () { - var context = this, args = arguments; - var throttler = function () { - timeout = null; - func.apply(context, args); - }; - if (debounce) clearTimeout(timeout); - if (debounce || !timeout) timeout = setTimeout(throttler, wait); - }; - } - - /** - * Define a jQuery plugin - */ - var dataID = "spectrum.id"; - $.fn.spectrum = function (opts, extra) { - - if (typeof opts == "string") { - - var returnValue = this; - var args = Array.prototype.slice.call( arguments, 1 ); - - this.each(function () { - var spect = spectrums[$(this).data(dataID)]; - if (spect) { - var method = spect[opts]; - if (!method) { - throw new Error( "Spectrum: no such method: '" + opts + "'" ); - } - - if (opts == "get") { - returnValue = spect.get(); - } - else if (opts == "container") { - returnValue = spect.container; - } - else if (opts == "option") { - returnValue = spect.option.apply(spect, args); - } - else if (opts == "destroy") { - spect.destroy(); - $(this).removeData(dataID); - } - else { - method.apply(spect, args); - } - } - }); - - return returnValue; - } - - // Initializing a new instance of spectrum - return this.spectrum("destroy").each(function () { - var options = $.extend({}, opts, $(this).data()); - var spect = spectrum(this, options); - $(this).data(dataID, spect.id); - }); - }; - - $.fn.spectrum.load = true; - $.fn.spectrum.loadOpts = {}; - $.fn.spectrum.draggable = draggable; - $.fn.spectrum.defaults = defaultOpts; - - $.spectrum = { }; - $.spectrum.localization = { }; - $.spectrum.palettes = { }; - - $.fn.spectrum.processNativeColorInputs = function () { - if (!inputTypeColorSupport) { - $("input[type=color]").spectrum({ - preferredFormat: "hex6" - }); - } - }; - - // TinyColor v1.1.1 - // 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._originalInput = 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; - }, - getOriginalInput: function() { - return this._originalInput; - }, - 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", - rebeccapurple: "663399", - 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), - hsva: new RegExp("hsva" + PERMISSIVE_MATCH4), - 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.hsva.exec(color))) { - return { h: match[1], s: match[2], v: match[3], a: match[4] }; - } - 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; - } - - window.tinycolor = tinycolor; - })(); - - - $(function () { - if ($.fn.spectrum.load) { - $.fn.spectrum.processNativeColorInputs(); - } - }); - -}); +!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports&&"object"==typeof module?module.exports=a(require("jquery")):a(jQuery)}(function(a,b){"use strict";function c(b,c,d,e){for(var f=[],g=0;g')}else{var m="sp-clear-display";f.push(a("
").append(a('').attr("title",e.noColorSelectedText)).html())}}return"
"+f.join("")+"
"}function d(){for(var a=0;aMath.abs(b-e);ta=f?"x":"y"}}else ta=null;var g=!ta||"x"===ta,h=!ta||"y"===ta;g&&(ka=parseFloat(a/ba)),h&&(la=parseFloat((ca-b)/ca)),Xa=!1,U.showAlpha||(ma=1),K()},y,z),Ta?(H(Ta),L(),Va=U.preferredFormat||tinycolor(Ta).format,u(Ta)):L(),V&&C();var d=p?"mousedown.spectrum":"click.spectrum touchstart.spectrum";Ha.delegate(".sp-thumb-el",d,b),Ia.delegate(".sp-thumb-el:nth-child(1)",d,{ignore:!0},b)}function t(){if(X&&window.localStorage){try{var b=window.localStorage[X].split(",#");b.length>1&&(delete window.localStorage[X],a.each(b,function(a,b){u(b)}))}catch(c){}try{qa=window.localStorage[X].split(";")}catch(c){}}}function u(b){if(W){var c=tinycolor(b).toRgbString();if(!pa[c]&&-1===a.inArray(c,qa))for(qa.push(c);qa.length>ra;)qa.shift();if(X&&window.localStorage)try{window.localStorage[X]=qa.join(";")}catch(d){}}}function v(){var a=[];if(U.showPalette)for(var b=0;b=ca||0>=ba||0>=ea)&&O(),aa=!0,xa.addClass(sa),ta=null,va.trigger("dragstart.spectrum",[I()])}function z(){aa=!1,xa.removeClass(sa),va.trigger("dragstop.spectrum",[I()])}function A(){var a=Ga.val();if(null!==a&&""!==a||!Ya){var b=tinycolor(a);b.isValid()?(H(b),N(!0)):Ga.addClass("sp-validation-error")}else H(null),N(!0)}function B(){_?F():C()}function C(){var b=a.Event("beforeShow.spectrum");return _?void O():(va.trigger(b,[I()]),void(Z.beforeShow(I())===!1||b.isDefaultPrevented()||(d(),_=!0,a(ua).bind("keydown.spectrum",D),a(ua).bind("click.spectrum",E),a(window).bind("resize.spectrum",$),Qa.addClass("sp-active"),xa.removeClass("sp-hidden"),O(),L(),Ua=I(),x(),Z.show(Ua),va.trigger("show.spectrum",[Ua]))))}function D(a){27===a.keyCode&&F()}function E(a){2!=a.button&&(aa||(Wa?N(!0):G(),F()))}function F(){_&&!V&&(_=!1,a(ua).unbind("keydown.spectrum",D),a(ua).unbind("click.spectrum",E),a(window).unbind("resize.spectrum",$),Qa.removeClass("sp-active"),xa.addClass("sp-hidden"),Z.hide(I()),va.trigger("hide.spectrum",[I()]))}function G(){H(Ua,!0)}function H(a,b){if(tinycolor.equals(a,I()))return void L();var c,d;!a&&Ya?Xa=!0:(Xa=!1,c=tinycolor(a),d=c.toHsv(),ja=d.h%360/360,ka=d.s,la=d.v,ma=d.a),L(),c&&c.isValid()&&!b&&(Va=U.preferredFormat||c.getFormat())}function I(a){return a=a||{},Ya&&Xa?null:tinycolor.fromRatio({h:ja,s:ka,v:la,a:Math.round(100*ma)/100},{format:a.format||Va})}function J(){return!Ga.hasClass("sp-validation-error")}function K(){L(),Z.move(I()),va.trigger("move.spectrum",[I()])}function L(){Ga.removeClass("sp-validation-error"),M();var a=tinycolor.fromRatio({h:ja,s:1,v:1});za.css("background-color",a.toHexString());var b=Va;1>ma&&(0!==ma||"name"!==b)&&("hex"===b||"hex3"===b||"hex6"===b||"name"===b)&&(b="rgb");var c=I({format:b}),d="";if(Sa.removeClass("sp-clear-display"),Sa.css("background-color","transparent"),!c&&Ya)Sa.addClass("sp-clear-display");else{var e=c.toHexString(),f=c.toRgbString();if(q||1===c.alpha?Sa.css("background-color",f):(Sa.css("background-color","transparent"),Sa.css("filter",c.toFilter())),U.showAlpha){var g=c.toRgb();g.a=0;var h=tinycolor(g).toRgbString(),i="linear-gradient(left, "+h+", "+e+")";p?Da.css("filter",tinycolor(h).toFilter({gradientType:1},e)):(Da.css("background","-webkit-"+i),Da.css("background","-moz-"+i),Da.css("background","-ms-"+i),Da.css("background","linear-gradient(to right, "+h+", "+e+")"))}d=c.toString(b)}U.showInput&&Ga.val(d),U.showPalette&&w(),x()}function M(){var a=ka,b=la;if(Ya&&Xa)Fa.hide(),Ca.hide(),Aa.hide();else{Fa.show(),Ca.show(),Aa.show();var c=a*ba,d=ca-b*ca;c=Math.max(-da,Math.min(ba-da,c-da)),d=Math.max(-da,Math.min(ca-da,d-da)),Aa.css({top:d+"px",left:c+"px"});var e=ma*ga;Fa.css({left:e-ha/2+"px"});var f=ja*ea;Ca.css({top:f-ia+"px"})}}function N(a){var b=I(),c="",d=!tinycolor.equals(b,Ua);b&&(c=b.toString(Va),u(b)),Na&&va.val(c),a&&d&&(Z.change(b),va.trigger("change",[b]))}function O(){_&&(ba=za.width(),ca=za.height(),da=Aa.height(),fa=Ba.width(),ea=Ba.height(),ia=Ca.height(),ga=Ea.width(),ha=Fa.width(),V||(xa.css("position","absolute"),U.offset?xa.offset(U.offset):xa.offset(g(xa,Ra))),M(),U.showPalette&&w(),va.trigger("reflow.spectrum"))}function P(){va.show(),Ra.unbind("click.spectrum touchstart.spectrum"),xa.remove(),Qa.remove(),o[Za.id]=null}function Q(c,d){return c===b?a.extend({},U):d===b?U[c]:(U[c]=d,"preferredFormat"===c&&(Va=U.preferredFormat),void j())}function R(){wa=!1,va.attr("disabled",!1),Ra.removeClass("sp-disabled")}function S(){F(),wa=!0,va.attr("disabled",!0),Ra.addClass("sp-disabled")}function T(a){U.offset=a,O()}var U=e(h,f),V=U.flat,W=U.showSelectionPalette,X=U.localStorageKey,Y=U.theme,Z=U.callbacks,$=l(O,10),_=!1,aa=!1,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=1,na=[],oa=[],pa={},qa=U.selectionPalette.slice(0),ra=U.maxSelectionSize,sa="sp-dragging",ta=null,ua=f.ownerDocument,va=(ua.body,a(f)),wa=!1,xa=a(s,ua).addClass(Y),ya=xa.find(".sp-picker-container"),za=xa.find(".sp-color"),Aa=xa.find(".sp-dragger"),Ba=xa.find(".sp-hue"),Ca=xa.find(".sp-slider"),Da=xa.find(".sp-alpha-inner"),Ea=xa.find(".sp-alpha"),Fa=xa.find(".sp-alpha-handle"),Ga=xa.find(".sp-input"),Ha=xa.find(".sp-palette"),Ia=xa.find(".sp-initial"),Ja=xa.find(".sp-cancel"),Ka=xa.find(".sp-clear"),La=xa.find(".sp-choose"),Ma=xa.find(".sp-palette-toggle"),Na=va.is("input"),Oa=Na&&"color"===va.attr("type")&&m(),Pa=Na&&!V,Qa=Pa?a(r).addClass(Y).addClass(U.className).addClass(U.replacerClassName):a([]),Ra=Pa?Qa:va,Sa=Qa.find(".sp-preview-inner"),Ta=U.color||Na&&va.val(),Ua=!1,Va=U.preferredFormat,Wa=!U.showButtons||U.clickoutFiresChange,Xa=!Ta,Ya=U.allowEmpty&&!Oa;n();var Za={show:C,hide:F,toggle:B,reflow:O,option:Q,enable:R,disable:S,offset:T,set:function(a){H(a),N()},get:I,destroy:P,container:xa};return Za.id=o.push(Za)-1,Za}function g(b,c){var d=0,e=b.outerWidth(),f=b.outerHeight(),g=c.outerHeight(),h=b[0].ownerDocument,i=h.documentElement,j=i.clientWidth+a(h).scrollLeft(),k=i.clientHeight+a(h).scrollTop(),l=c.offset();return l.top+=g,l.left-=Math.min(l.left,l.left+e>j&&j>e?Math.abs(l.left+e-j):0),l.top-=Math.min(l.top,l.top+f>k&&k>f?Math.abs(f+g-d):d),l}function h(){}function i(a){a.stopPropagation()}function j(a,b){var c=Array.prototype.slice,d=c.call(arguments,2);return function(){return a.apply(b,d.concat(c.call(arguments)))}}function k(b,c,d,e){function f(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.returnValue=!1}function g(a){if(k){if(p&&j.documentMode<9&&!a.button)return i();var d=a.originalEvent&&a.originalEvent.touches&&a.originalEvent.touches[0],e=d&&d.pageX||a.pageX,g=d&&d.pageY||a.pageY,h=Math.max(0,Math.min(e-l.left,n)),q=Math.max(0,Math.min(g-l.top,m));o&&f(a),c.apply(b,[h,q,a])}}function h(c){var e=c.which?3==c.which:2==c.button;e||k||d.apply(b,arguments)!==!1&&(k=!0,m=a(b).height(),n=a(b).width(),l=a(b).offset(),a(j).bind(q),a(j.body).addClass("sp-dragging"),g(c),f(c))}function i(){k&&(a(j).unbind(q),a(j.body).removeClass("sp-dragging"),setTimeout(function(){e.apply(b,arguments)},0)),k=!1}c=c||function(){},d=d||function(){},e=e||function(){};var j=document,k=!1,l={},m=0,n=0,o="ontouchstart"in window,q={};q.selectstart=f,q.dragstart=f,q["touchmove mousemove"]=g,q["touchend mouseup"]=i,a(b).bind("touchstart mousedown",h)}function l(a,b,c){var d;return function(){var e=this,f=arguments,g=function(){d=null,a.apply(e,f)};c&&clearTimeout(d),(c||!d)&&(d=setTimeout(g,b))}}function m(){return a.fn.spectrum.inputTypeColorSupport()}var n={beforeShow:h,move:h,change:h,show:h,hide:h,color:!1,flat:!1,showInput:!1,allowEmpty:!1,showButtons:!0,clickoutFiresChange:!0,showInitial:!1,showPalette:!1,showPaletteOnly:!1,hideAfterPaletteSelect:!1,togglePaletteOnly:!1,showSelectionPalette:!0,localStorageKey:!1,appendTo:"body",maxSelectionSize:7,cancelText:"cancel",chooseText:"choose",togglePaletteMoreText:"more",togglePaletteLessText:"less",clearText:"Clear Color Selection",noColorSelectedText:"No Color Selected",preferredFormat:!1,className:"",containerClassName:"",replacerClassName:"",showAlpha:!1,theme:"sp-light",palette:[["#ffffff","#000000","#ff0000","#ff8000","#ffff00","#008000","#0000ff","#4b0082","#9400d3"]],selectionPalette:[],disabled:!1,offset:null},o=[],p=!!/msie/i.exec(window.navigator.userAgent),q=function(){function a(a,b){return!!~(""+a).indexOf(b)}var b=document.createElement("div"),c=b.style;return c.cssText="background-color:rgba(0,0,0,.5)",a(c.backgroundColor,"rgba")||a(c.backgroundColor,"hsla")}(),r=["
","
","
","
"].join(""),s=function(){var a="";if(p)for(var b=1;6>=b;b++)a+="
";return["
","
","
","
","","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
",a,"
","
","
","
","
","","
","
","
","","","
","
","
"].join("")}(),t="spectrum.id";a.fn.spectrum=function(b,c){if("string"==typeof b){var d=this,e=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=o[a(this).data(t)];if(c){var f=c[b];if(!f)throw new Error("Spectrum: no such method: '"+b+"'");"get"==b?d=c.get():"container"==b?d=c.container:"option"==b?d=c.option.apply(c,e):"destroy"==b?(c.destroy(),a(this).removeData(t)):f.apply(c,e)}}),d}return this.spectrum("destroy").each(function(){var c=a.extend({},b,a(this).data()),d=f(this,c);a(this).data(t,d.id)})},a.fn.spectrum.load=!0,a.fn.spectrum.loadOpts={},a.fn.spectrum.draggable=k,a.fn.spectrum.defaults=n,a.fn.spectrum.inputTypeColorSupport=function u(){if("undefined"==typeof u._cachedResult){var b=a("")[0];u._cachedResult="color"===b.type&&""!==b.value}return u._cachedResult},a.spectrum={},a.spectrum.localization={},a.spectrum.palettes={},a.fn.spectrum.processNativeColorInputs=function(){var b=a("input[type=color]");b.length&&!m()&&b.spectrum({preferredFormat:"hex6"})},function(){function a(a){var c={r:0,g:0,b:0},e=1,g=!1,h=!1;return"string"==typeof a&&(a=G(a)),"object"==typeof a&&(a.hasOwnProperty("r")&&a.hasOwnProperty("g")&&a.hasOwnProperty("b")?(c=b(a.r,a.g,a.b),g=!0,h="%"===String(a.r).substr(-1)?"prgb":"rgb"):a.hasOwnProperty("h")&&a.hasOwnProperty("s")&&a.hasOwnProperty("v")?(a.s=D(a.s),a.v=D(a.v),c=f(a.h,a.s,a.v),g=!0,h="hsv"):a.hasOwnProperty("h")&&a.hasOwnProperty("s")&&a.hasOwnProperty("l")&&(a.s=D(a.s),a.l=D(a.l),c=d(a.h,a.s,a.l),g=!0,h="hsl"),a.hasOwnProperty("a")&&(e=a.a)),e=w(e),{ok:g,format:a.format||h,r:M(255,N(c.r,0)),g:M(255,N(c.g,0)),b:M(255,N(c.b,0)),a:e}}function b(a,b,c){return{r:255*x(a,255),g:255*x(b,255),b:255*x(c,255)}}function c(a,b,c){a=x(a,255),b=x(b,255),c=x(c,255);var d,e,f=N(a,b,c),g=M(a,b,c),h=(f+g)/2;if(f==g)d=e=0;else{var i=f-g;switch(e=h>.5?i/(2-f-g):i/(f+g),f){case a:d=(b-c)/i+(c>b?6:0);break;case b:d=(c-a)/i+2;break;case c:d=(a-b)/i+4}d/=6}return{h:d,s:e,l:h}}function d(a,b,c){function d(a,b,c){return 0>c&&(c+=1),c>1&&(c-=1),1/6>c?a+6*(b-a)*c:.5>c?b:2/3>c?a+(b-a)*(2/3-c)*6:a}var e,f,g;if(a=x(a,360),b=x(b,100),c=x(c,100),0===b)e=f=g=c;else{var h=.5>c?c*(1+b):c+b-c*b,i=2*c-h;e=d(i,h,a+1/3),f=d(i,h,a),g=d(i,h,a-1/3)}return{r:255*e,g:255*f,b:255*g}}function e(a,b,c){a=x(a,255),b=x(b,255),c=x(c,255);var d,e,f=N(a,b,c),g=M(a,b,c),h=f,i=f-g;if(e=0===f?0:i/f,f==g)d=0;else{switch(f){case a:d=(b-c)/i+(c>b?6:0);break;case b:d=(c-a)/i+2;break;case c:d=(a-b)/i+4}d/=6}return{h:d,s:e,v:h}}function f(a,b,c){a=6*x(a,360),b=x(b,100),c=x(c,100);var d=K.floor(a),e=a-d,f=c*(1-b),g=c*(1-e*b),h=c*(1-(1-e)*b),i=d%6,j=[c,g,f,f,h,c][i],k=[h,c,c,g,f,f][i],l=[f,f,h,c,c,g][i];return{r:255*j,g:255*k,b:255*l}}function g(a,b,c,d){var e=[C(L(a).toString(16)),C(L(b).toString(16)),C(L(c).toString(16))];return d&&e[0].charAt(0)==e[0].charAt(1)&&e[1].charAt(0)==e[1].charAt(1)&&e[2].charAt(0)==e[2].charAt(1)?e[0].charAt(0)+e[1].charAt(0)+e[2].charAt(0):e.join("")}function h(a,b,c,d){var e=[C(E(d)),C(L(a).toString(16)),C(L(b).toString(16)),C(L(c).toString(16))];return e.join("")}function i(a,b){b=0===b?0:b||10;var c=P(a).toHsl();return c.s-=b/100,c.s=y(c.s),P(c)}function j(a,b){b=0===b?0:b||10;var c=P(a).toHsl();return c.s+=b/100,c.s=y(c.s),P(c)}function k(a){return P(a).desaturate(100)}function l(a,b){b=0===b?0:b||10;var c=P(a).toHsl();return c.l+=b/100,c.l=y(c.l),P(c)}function m(a,b){b=0===b?0:b||10;var c=P(a).toRgb();return c.r=N(0,M(255,c.r-L(255*-(b/100)))),c.g=N(0,M(255,c.g-L(255*-(b/100)))),c.b=N(0,M(255,c.b-L(255*-(b/100)))),P(c)}function n(a,b){b=0===b?0:b||10;var c=P(a).toHsl();return c.l-=b/100,c.l=y(c.l),P(c)}function o(a,b){var c=P(a).toHsl(),d=(L(c.h)+b)%360;return c.h=0>d?360+d:d,P(c)}function p(a){var b=P(a).toHsl();return b.h=(b.h+180)%360,P(b)}function q(a){var b=P(a).toHsl(),c=b.h;return[P(a),P({h:(c+120)%360,s:b.s,l:b.l}),P({h:(c+240)%360,s:b.s,l:b.l})]}function r(a){var b=P(a).toHsl(),c=b.h;return[P(a),P({h:(c+90)%360,s:b.s,l:b.l}),P({h:(c+180)%360,s:b.s,l:b.l}),P({h:(c+270)%360,s:b.s,l:b.l})]}function s(a){var b=P(a).toHsl(),c=b.h;return[P(a),P({h:(c+72)%360,s:b.s,l:b.l}),P({h:(c+216)%360,s:b.s,l:b.l})]}function t(a,b,c){b=b||6,c=c||30;var d=P(a).toHsl(),e=360/c,f=[P(a)];for(d.h=(d.h-(e*b>>1)+720)%360;--b;)d.h=(d.h+e)%360,f.push(P(d));return f}function u(a,b){b=b||6;for(var c=P(a).toHsv(),d=c.h,e=c.s,f=c.v,g=[],h=1/b;b--;)g.push(P({h:d,s:e,v:f})),f=(f+h)%1;return g}function v(a){var b={};for(var c in a)a.hasOwnProperty(c)&&(b[a[c]]=c);return b}function w(a){return a=parseFloat(a),(isNaN(a)||0>a||a>1)&&(a=1),a}function x(a,b){A(a)&&(a="100%");var c=B(a);return a=M(b,N(0,parseFloat(a))),c&&(a=parseInt(a*b,10)/100),K.abs(a-b)<1e-6?1:a%b/parseFloat(b)}function y(a){return M(1,N(0,a))}function z(a){return parseInt(a,16)}function A(a){return"string"==typeof a&&-1!=a.indexOf(".")&&1===parseFloat(a)}function B(a){return"string"==typeof a&&-1!=a.indexOf("%")}function C(a){return 1==a.length?"0"+a:""+a}function D(a){return 1>=a&&(a=100*a+"%"),a}function E(a){return Math.round(255*parseFloat(a)).toString(16)}function F(a){return z(a)/255}function G(a){a=a.replace(H,"").replace(I,"").toLowerCase();var b=!1;if(Q[a])a=Q[a],b=!0;else if("transparent"==a)return{r:0,g:0,b:0,a:0,format:"name"};var c;return(c=S.rgb.exec(a))?{r:c[1],g:c[2],b:c[3]}:(c=S.rgba.exec(a))?{r:c[1],g:c[2],b:c[3],a:c[4]}:(c=S.hsl.exec(a))?{h:c[1],s:c[2],l:c[3]}:(c=S.hsla.exec(a))?{h:c[1],s:c[2],l:c[3],a:c[4]}:(c=S.hsv.exec(a))?{h:c[1],s:c[2],v:c[3]}:(c=S.hsva.exec(a))?{h:c[1],s:c[2],v:c[3],a:c[4]}:(c=S.hex8.exec(a))?{a:F(c[1]),r:z(c[2]),g:z(c[3]),b:z(c[4]),format:b?"name":"hex8"}:(c=S.hex6.exec(a))?{r:z(c[1]),g:z(c[2]),b:z(c[3]),format:b?"name":"hex"}:(c=S.hex3.exec(a))?{r:z(c[1]+""+c[1]),g:z(c[2]+""+c[2]),b:z(c[3]+""+c[3]),format:b?"name":"hex"}:!1}var H=/^[\s,#]+/,I=/\s+$/,J=0,K=Math,L=K.round,M=K.min,N=K.max,O=K.random,P=function(b,c){if(b=b?b:"",c=c||{},b instanceof P)return b;if(!(this instanceof P))return new P(b,c);var d=a(b);this._originalInput=b,this._r=d.r,this._g=d.g,this._b=d.b,this._a=d.a,this._roundA=L(100*this._a)/100,this._format=c.format||d.format,this._gradientType=c.gradientType,this._r<1&&(this._r=L(this._r)),this._g<1&&(this._g=L(this._g)),this._b<1&&(this._b=L(this._b)),this._ok=d.ok,this._tc_id=J++};P.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var a=this.toRgb();return(299*a.r+587*a.g+114*a.b)/1e3},setAlpha:function(a){return this._a=w(a),this._roundA=L(100*this._a)/100,this},toHsv:function(){var a=e(this._r,this._g,this._b);return{h:360*a.h,s:a.s,v:a.v,a:this._a}},toHsvString:function(){var a=e(this._r,this._g,this._b),b=L(360*a.h),c=L(100*a.s),d=L(100*a.v);return 1==this._a?"hsv("+b+", "+c+"%, "+d+"%)":"hsva("+b+", "+c+"%, "+d+"%, "+this._roundA+")"},toHsl:function(){var a=c(this._r,this._g,this._b);return{h:360*a.h,s:a.s,l:a.l,a:this._a}},toHslString:function(){var a=c(this._r,this._g,this._b),b=L(360*a.h),d=L(100*a.s),e=L(100*a.l);return 1==this._a?"hsl("+b+", "+d+"%, "+e+"%)":"hsla("+b+", "+d+"%, "+e+"%, "+this._roundA+")"},toHex:function(a){return g(this._r,this._g,this._b,a)},toHexString:function(a){return"#"+this.toHex(a)},toHex8:function(){return h(this._r,this._g,this._b,this._a)},toHex8String:function(){return"#"+this.toHex8()},toRgb:function(){return{r:L(this._r),g:L(this._g),b:L(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+L(this._r)+", "+L(this._g)+", "+L(this._b)+")":"rgba("+L(this._r)+", "+L(this._g)+", "+L(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:L(100*x(this._r,255))+"%",g:L(100*x(this._g,255))+"%",b:L(100*x(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+L(100*x(this._r,255))+"%, "+L(100*x(this._g,255))+"%, "+L(100*x(this._b,255))+"%)":"rgba("+L(100*x(this._r,255))+"%, "+L(100*x(this._g,255))+"%, "+L(100*x(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":this._a<1?!1:R[g(this._r,this._g,this._b,!0)]||!1},toFilter:function(a){var b="#"+h(this._r,this._g,this._b,this._a),c=b,d=this._gradientType?"GradientType = 1, ":"";if(a){var e=P(a);c=e.toHex8String()}return"progid:DXImageTransform.Microsoft.gradient("+d+"startColorstr="+b+",endColorstr="+c+")"},toString:function(a){var b=!!a;a=a||this._format;var c=!1,d=this._a<1&&this._a>=0,e=!b&&d&&("hex"===a||"hex6"===a||"hex3"===a||"name"===a);return e?"name"===a&&0===this._a?this.toName():this.toRgbString():("rgb"===a&&(c=this.toRgbString()),"prgb"===a&&(c=this.toPercentageRgbString()),("hex"===a||"hex6"===a)&&(c=this.toHexString()),"hex3"===a&&(c=this.toHexString(!0)),"hex8"===a&&(c=this.toHex8String()),"name"===a&&(c=this.toName()),"hsl"===a&&(c=this.toHslString()),"hsv"===a&&(c=this.toHsvString()),c||this.toHexString())},_applyModification:function(a,b){var c=a.apply(null,[this].concat([].slice.call(b)));return this._r=c._r,this._g=c._g,this._b=c._b,this.setAlpha(c._a),this},lighten:function(){return this._applyModification(l,arguments)},brighten:function(){return this._applyModification(m,arguments)},darken:function(){return this._applyModification(n,arguments)},desaturate:function(){return this._applyModification(i,arguments)},saturate:function(){return this._applyModification(j,arguments)},greyscale:function(){return this._applyModification(k,arguments)},spin:function(){return this._applyModification(o,arguments)},_applyCombination:function(a,b){return a.apply(null,[this].concat([].slice.call(b)))},analogous:function(){return this._applyCombination(t,arguments)},complement:function(){return this._applyCombination(p,arguments)},monochromatic:function(){return this._applyCombination(u,arguments)},splitcomplement:function(){return this._applyCombination(s,arguments)},triad:function(){return this._applyCombination(q,arguments)},tetrad:function(){return this._applyCombination(r,arguments)}},P.fromRatio=function(a,b){if("object"==typeof a){var c={};for(var d in a)a.hasOwnProperty(d)&&("a"===d?c[d]=a[d]:c[d]=D(a[d]));a=c}return P(a,b)},P.equals=function(a,b){return a&&b?P(a).toRgbString()==P(b).toRgbString():!1},P.random=function(){return P.fromRatio({r:O(),g:O(),b:O()})},P.mix=function(a,b,c){c=0===c?0:c||50;var d,e=P(a).toRgb(),f=P(b).toRgb(),g=c/100,h=2*g-1,i=f.a-e.a;d=h*i==-1?h:(h+i)/(1+h*i),d=(d+1)/2;var j=1-d,k={r:f.r*d+e.r*j,g:f.g*d+e.g*j,b:f.b*d+e.b*j,a:f.a*g+e.a*(1-g)};return P(k)},P.readability=function(a,b){var c=P(a),d=P(b),e=c.toRgb(),f=d.toRgb(),g=c.getBrightness(),h=d.getBrightness(),i=Math.max(e.r,f.r)-Math.min(e.r,f.r)+Math.max(e.g,f.g)-Math.min(e.g,f.g)+Math.max(e.b,f.b)-Math.min(e.b,f.b);return{brightness:Math.abs(g-h),color:i}},P.isReadable=function(a,b){var c=P.readability(a,b);return c.brightness>125&&c.color>500},P.mostReadable=function(a,b){for(var c=null,d=0,e=!1,f=0;f125&&g.color>500,i=3*(g.brightness/125)+g.color/500;(h&&!e||h&&e&&i>d||!h&&!e&&i>d)&&(e=h,d=i,c=P(b[f]))}return c};var Q=P.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",rebeccapurple:"663399",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"},R=P.hexNames=v(Q),S=function(){var a="[-\\+]?\\d+%?",b="[-\\+]?\\d*\\.\\d+%?",c="(?:"+b+")|(?:"+a+")",d="[\\s|\\(]+("+c+")[,|\\s]+("+c+")[,|\\s]+("+c+")\\s*\\)?",e="[\\s|\\(]+("+c+")[,|\\s]+("+c+")[,|\\s]+("+c+")[,|\\s]+("+c+")\\s*\\)?";return{rgb:new RegExp("rgb"+d),rgba:new RegExp("rgba"+e),hsl:new RegExp("hsl"+d),hsla:new RegExp("hsla"+e),hsv:new RegExp("hsv"+d),hsva:new RegExp("hsva"+e),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})$/}}();window.tinycolor=P}(),a(function(){a.fn.spectrum.load&&a.fn.spectrum.processNativeColorInputs()})}); \ No newline at end of file