From 103a5c9252676bc2055a3de7e5d33a1b81c34b57 Mon Sep 17 00:00:00 2001 From: siinus Date: Sun, 8 Nov 2020 01:47:25 +0200 Subject: [PATCH] Add python led programs --- pyleds/.gitignore | 1 + pyleds/lib/FakeStrip.py | 94 + pyleds/lib/Litsimaja.py | 33 + pyleds/lib/LoopSwitch.py | 11 + pyleds/lib/Program.py | 17 + pyleds/lib/ProgramLoading.py | 40 + pyleds/program/siinus/Static.py | 13 + pyleds/program/siinus/Wipes.py | 37 + pyleds/run.py | 57 + pyleds/templates/index.html | 103 + pyleds/templates/jscolor.js | 3180 +++++++++++++++++++++++++++++++ 11 files changed, 3586 insertions(+) create mode 100644 pyleds/.gitignore create mode 100644 pyleds/lib/FakeStrip.py create mode 100644 pyleds/lib/Litsimaja.py create mode 100644 pyleds/lib/LoopSwitch.py create mode 100644 pyleds/lib/Program.py create mode 100644 pyleds/lib/ProgramLoading.py create mode 100644 pyleds/program/siinus/Static.py create mode 100644 pyleds/program/siinus/Wipes.py create mode 100755 pyleds/run.py create mode 100644 pyleds/templates/index.html create mode 100644 pyleds/templates/jscolor.js diff --git a/pyleds/.gitignore b/pyleds/.gitignore new file mode 100644 index 0000000..7d264db --- /dev/null +++ b/pyleds/.gitignore @@ -0,0 +1 @@ +litsimaja.log diff --git a/pyleds/lib/FakeStrip.py b/pyleds/lib/FakeStrip.py new file mode 100644 index 0000000..ad20338 --- /dev/null +++ b/pyleds/lib/FakeStrip.py @@ -0,0 +1,94 @@ +from rpi_ws281x import PixelStrip +import atexit + + +class _LedData(object): + _led: [] + + def __init__(self, channel, size): + self.size = size + self.channel = channel + + def __getitem__(self, pos): + """Return the 24-bit RGB color value at the provided position or slice + of positions. + """ + # Handle if a slice of positions are passed in by grabbing all the values + # and returning them in a list. + if isinstance(pos, slice): + return [self._led[n] for n in range(*pos.indices(self.size))] + # Else assume the passed in value is a number to the position. + else: + return self._led[pos] + + def __setitem__(self, pos, value): + """Set the 24-bit RGB color value at the provided position or slice of + positions. + """ + # Handle if a slice of positions are passed in by setting the appropriate + # LED data values to the provided values. + if isinstance(pos, slice): + index = 0 + for n in range(*pos.indices(self.size)): + self._led[n] = value[index] + index += 1 + # Else assume the passed in value is a number to the position. + # else: + self._led[pos] = value + + +class FakeStrip(PixelStrip): + _brightness: int + + def __init__(self, num, pin, freq_hz=800000, dma=10, invert=False, + brightness=255, channel=0, strip_type=None, gamma=None): + + if gamma is None: + # Support gamma in place of strip_type for back-compat with + # previous version of forked library + if type(strip_type) is list and len(strip_type) == 256: + gamma = strip_type + # strip_type = None + else: + gamma = list(range(256)) + + # Initialize the channel in use + self._channel = None + self._gamma = gamma + self._brightness = brightness + + # Grab the led data array. + self._led_data = _LedData(self._channel, num) + + # Substitute for __del__, traps an exit condition and cleans up properly + atexit.register(self._cleanup) + + def _cleanup(self): + # Clean up memory used by the library when not needed anymore. + if self._channel is not None: + self._channel = None + + def setGamma(self, gamma): + if type(gamma) is list and len(gamma) == 256: + self._gamma = gamma + + def begin(self): + """Initialize library, must be called once before other functions are called.""" + return + + def show(self): + """Update the display with the data from the LED buffer.""" + return # render + + def getBrightness(self) -> int: + return self._brightness + + def setBrightness(self, brightness: int): + """Scale each LED in the buffer by the provided brightness. + A brightness of 0 is the darkest and 255 is the brightest. + """ + self._brightness = brightness + + def numPixels(self): + """Return the number of pixels in the display.""" + return self._led_data.size diff --git a/pyleds/lib/Litsimaja.py b/pyleds/lib/Litsimaja.py new file mode 100644 index 0000000..6544cef --- /dev/null +++ b/pyleds/lib/Litsimaja.py @@ -0,0 +1,33 @@ +from rpi_ws281x import PixelStrip +from lib.FakeStrip import FakeStrip +from lib.LoopSwitch import LoopSwitch + + +class Litsimaja(object): + _strip: PixelStrip + _loops: [] + + def __init__(self): + self._strip = FakeStrip(290, 18, 800000, 10, False, 255, 0, 4104) + self._loops = [] + self._strip.begin() + + def count_pixels(self) -> int: + return self._strip.numPixels() + + def get_strip(self) -> PixelStrip: + return self._strip + + def set_pixel_color(self, n: int, color: int) -> None: + self._strip.setPixelColor(n, color) + + def show(self) -> None: + self._strip.show() + + def add_loop(self, loop: LoopSwitch): + self._loops.append(loop) + + def clear_loops(self): + for loop in self._loops: + loop.stop() + self._loops.clear() diff --git a/pyleds/lib/LoopSwitch.py b/pyleds/lib/LoopSwitch.py new file mode 100644 index 0000000..a7bd271 --- /dev/null +++ b/pyleds/lib/LoopSwitch.py @@ -0,0 +1,11 @@ +class LoopSwitch(object): + _run: bool + + def __init__(self): + self._run = True + + def stop(self): + self._run = False + + def status(self) -> bool: + return self._run diff --git a/pyleds/lib/Program.py b/pyleds/lib/Program.py new file mode 100644 index 0000000..57f7c20 --- /dev/null +++ b/pyleds/lib/Program.py @@ -0,0 +1,17 @@ +from lib.Litsimaja import Litsimaja +from lib.LoopSwitch import LoopSwitch + + +class Program: + _lm: Litsimaja + _loop: LoopSwitch + + def __init__(self, lm: Litsimaja): + self._lm = lm + self._loop = LoopSwitch() + + def get_loop(self): + return self._loop + + def run(self, args: [] = None): + pass diff --git a/pyleds/lib/ProgramLoading.py b/pyleds/lib/ProgramLoading.py new file mode 100644 index 0000000..ede2b53 --- /dev/null +++ b/pyleds/lib/ProgramLoading.py @@ -0,0 +1,40 @@ +from os import scandir +from lib.Litsimaja import Litsimaja +from lib.Program import Program + + +def resolve(namespace: str, class_name: str): + m_name = 'program.' + namespace + '.' + class_name + module = __import__(m_name, None, None, [m_name]) + return module + + +def run(namespace: str, class_name: str, lm: Litsimaja, logger, args: [] = None): + module = resolve(namespace, class_name) + loaded_class = getattr(module, class_name) + program = loaded_class(lm) + logger.info('Loaded "' + module.name() + '" from ' + namespace + '.' + class_name + ' with args: ' + repr(args)) + lm.add_loop(program.get_loop()) + program.run(args) + + +def list_all(grouped: bool = False): + all_programs = [] + programs = [] + for entry in scandir('program'): + if entry.is_dir(): + for file in scandir('program/' + entry.name): + if file.is_file() and file.name[-3:] == '.py': + try: + program_name = file.name[: -3] + module = resolve(entry.name, program_name) + if hasattr(module, program_name) and issubclass(getattr(module, program_name), Program): + programs.append({'prg': entry.name + '.' + program_name, 'name': module.name()}) + except AttributeError: + continue + if grouped and len(programs) > 0: + all_programs.append({'group': entry.name, 'programs': programs}) + programs = [] + if not grouped: + all_programs = programs + return all_programs diff --git a/pyleds/program/siinus/Static.py b/pyleds/program/siinus/Static.py new file mode 100644 index 0000000..67235e4 --- /dev/null +++ b/pyleds/program/siinus/Static.py @@ -0,0 +1,13 @@ +from lib.Program import Program + + +def name(): + return 'Static color' + + +class Static(Program): + def run(self, args: [] = None) -> None: + color_arr = args['color'] + for i in range(self._lm.count_pixels()): + self._lm.set_pixel_color(i, (color_arr[0] << 16) | (color_arr[1] << 8) | color_arr[2]) + self._lm.show() diff --git a/pyleds/program/siinus/Wipes.py b/pyleds/program/siinus/Wipes.py new file mode 100644 index 0000000..ac4cbb7 --- /dev/null +++ b/pyleds/program/siinus/Wipes.py @@ -0,0 +1,37 @@ +from lib.Program import Program +from lib.Litsimaja import Litsimaja +from rpi_ws281x import Color + + +def name(): + return 'Wipey wipe' + + +def color_wipe(lm: Litsimaja, color): + for i in range(lm.count_pixels()): + lm.set_pixel_color(i, color) + if i % 4 == 0: + lm.show() + lm.show() + + +class Wipes(Program): + def run(self, args=None): + loop = False + if 'loop' in args and args['loop']: + loop = args['loop'] + if 'color' in args: + end = Color(args['color'][0], args['color'][1], args['color'][2]) + else: + end = Color(100, 100, 50) + + r = Color(255, 0, 0) + g = Color(0, 255, 0) + b = Color(0, 0, 255) + while self.get_loop().status(): + color_wipe(self._lm, r) + color_wipe(self._lm, g) + color_wipe(self._lm, b) + color_wipe(self._lm, end) + if not loop: + break diff --git a/pyleds/run.py b/pyleds/run.py new file mode 100755 index 0000000..73a367a --- /dev/null +++ b/pyleds/run.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import logging +import sys +import json + +import lib.ProgramLoading as Pl +from lib.Litsimaja import Litsimaja +from flask import Flask, request, Response, render_template + +# logging +logger = logging.getLogger('litsimaja') +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler = logging.FileHandler('litsimaja.log') +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) +stdout_handler = logging.StreamHandler(sys.stdout) +stdout_handler.setFormatter(formatter) +logger.addHandler(stdout_handler) + +# start litsimaja +lm = Litsimaja() +app = Flask(__name__) +# Pl.run('siinus', 'Static', lm, logger, {'color': [50, 50, 50]}) + + +@app.route('/', methods=['GET']) +def respondroot(): + return render_template('index.html', programs=Pl.list_all(True)) + + +@app.route('/jscolor.js', methods=['GET']) +def respondjs(): + return render_template('jscolor.js') + + +@app.route('/run') +def run(): + Pl.run('siinus', 'MyProgram', lm, logger, {'color': [50, 50, 50]}) + return Response(status=200) + + +@app.route('/crash', methods=['GET']) +def crash(): + lm.clear_loops() + return Response(status=200) + + +@app.route('/program/', methods=['POST']) +def run_program(program): + args = request.get_json(force=True) + prg = program.split('.') + Pl.run(prg[0], prg[1], lm, logger, args) + return Response(status=200) + + +app.run('0.0.0.0', 8080) diff --git a/pyleds/templates/index.html b/pyleds/templates/index.html new file mode 100644 index 0000000..7e1763c --- /dev/null +++ b/pyleds/templates/index.html @@ -0,0 +1,103 @@ + + + litsimaja + + + + + + + + + + + + + + + + + diff --git a/pyleds/templates/jscolor.js b/pyleds/templates/jscolor.js new file mode 100644 index 0000000..9e78392 --- /dev/null +++ b/pyleds/templates/jscolor.js @@ -0,0 +1,3180 @@ +/** + * jscolor - JavaScript Color Picker + * + * @link http://jscolor.com + * @license For open source use: GPLv3 + * For commercial use: JSColor Commercial License + * @author Jan Odvarko - East Desire + * @version 2.3.3 + * + * See usage examples at http://jscolor.com/examples/ + */ + + +"use strict"; + + +if (!window.jscolor) { + +window.jscolor = (function () { // BEGIN window.jscolor + +var jsc = { + + + initialized : false, + + instances : [], // created instances of jscolor + + triggerQueue : [], // events waiting to be triggered after init + + + register : function () { + document.addEventListener('DOMContentLoaded', jsc.init, false); + document.addEventListener('mousedown', jsc.onDocumentMouseDown, false); + document.addEventListener('keyup', jsc.onDocumentKeyUp, false); + window.addEventListener('resize', jsc.onWindowResize, false); + }, + + + init : function () { + if (jsc.initialized) { + return; + } + + jsc.pub.install(); + jsc.initialized = true; + + // trigger events waiting in the queue + while (jsc.triggerQueue.length) { + var ev = jsc.triggerQueue.shift(); + jsc.triggerGlobal(ev); + } + }, + + + installBySelector : function (selector, rootNode) { + rootNode = rootNode ? jsc.node(rootNode) : document; + if (!rootNode) { + throw new Error('Missing root node'); + } + + var elms = rootNode.querySelectorAll(selector); + + // for backward compatibility with DEPRECATED installation/configuration using className + var matchClass = new RegExp('(^|\\s)(' + jsc.pub.lookupClass + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i'); + + for (var i = 0; i < elms.length; i += 1) { + + if (elms[i].jscolor && elms[i].jscolor instanceof jsc.pub) { + continue; // jscolor already installed on this element + } + + if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color' && jsc.isColorAttrSupported) { + continue; // skips inputs of type 'color' if supported by the browser + } + + var dataOpts, m; + + if ( + (dataOpts = jsc.getDataAttr(elms[i], 'jscolor')) !== null || + (elms[i].className && (m = elms[i].className.match(matchClass))) // installation using className (DEPRECATED) + ) { + var targetElm = elms[i]; + + var optsStr = ''; + if (dataOpts !== null) { + optsStr = dataOpts; + + } else if (m) { // installation using className (DEPRECATED) + console.warn('Installation using class name is DEPRECATED. Use data-jscolor="" attribute instead.' + jsc.docsRef); + if (m[4]) { + optsStr = m[4]; + } + } + + var opts = null; + if (optsStr.trim()) { + try { + opts = jsc.parseOptionsStr(optsStr); + } catch (e) { + console.warn(e + '\n' + optsStr); + } + } + + try { + new jsc.pub(targetElm, opts); + } catch (e) { + console.warn(e); + } + } + } + }, + + + parseOptionsStr : function (str) { + var opts = null; + + try { + opts = JSON.parse(str); + + } catch (eParse) { + if (!jsc.pub.looseJSON) { + throw new Error('Could not parse jscolor options as JSON: ' + eParse); + } else { + // loose JSON syntax is enabled -> try to evaluate the options string as JavaScript object + try { + opts = (new Function ('var opts = (' + str + '); return typeof opts === "object" ? opts : {};'))(); + } catch (eEval) { + throw new Error('Could not evaluate jscolor options: ' + eEval); + } + } + } + return opts; + }, + + + getInstances : function () { + var inst = []; + for (var i = 0; i < jsc.instances.length; i += 1) { + // if the targetElement still exists, the instance is considered "alive" + if (jsc.instances[i] && jsc.instances[i].targetElement) { + inst.push(jsc.instances[i]); + } + } + return inst; + }, + + + createEl : function (tagName) { + var el = document.createElement(tagName); + jsc.setData(el, 'gui', true) + return el; + }, + + + node : function (nodeOrSelector) { + if (!nodeOrSelector) { + return null; + } + + if (typeof nodeOrSelector === 'string') { + // query selector + var sel = nodeOrSelector; + var el = null; + try { + el = document.querySelector(sel); + } catch (e) { + console.warn(e); + return null; + } + if (!el) { + console.warn('No element matches the selector: %s', sel); + } + return el; + } + + if (jsc.isNode(nodeOrSelector)) { + // DOM node + return nodeOrSelector; + } + + console.warn('Invalid node of type %s: %s', typeof nodeOrSelector, nodeOrSelector); + return null; + }, + + + // See https://stackoverflow.com/questions/384286/ + isNode : function (val) { + if (typeof Node === 'object') { + return val instanceof Node; + } + return val && typeof val === 'object' && typeof val.nodeType === 'number' && typeof val.nodeName === 'string'; + }, + + + nodeName : function (node) { + if (node && node.nodeName) { + return node.nodeName.toLowerCase(); + } + return false; + }, + + + removeChildren : function (node) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + }, + + + isTextInput : function (el) { + return el && jsc.nodeName(el) === 'input' && el.type.toLowerCase() === 'text'; + }, + + + isButton : function (el) { + if (!el) { + return false; + } + var n = jsc.nodeName(el); + return ( + (n === 'button') || + (n === 'input' && ['button', 'submit', 'reset'].indexOf(el.type.toLowerCase()) > -1) + ); + }, + + + isButtonEmpty : function (el) { + switch (jsc.nodeName(el)) { + case 'input': return (!el.value || el.value.trim() === ''); + case 'button': return (el.textContent.trim() === ''); + } + return null; // could not determine element's text + }, + + + // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + isPassiveEventSupported : (function () { + var supported = false; + + try { + var opts = Object.defineProperty({}, 'passive', { + get: function () { supported = true; } + }); + window.addEventListener('testPassive', null, opts); + window.removeEventListener('testPassive', null, opts); + } catch (e) {} + + return supported; + })(), + + + isColorAttrSupported : (function () { + var elm = document.createElement('input'); + if (elm.setAttribute) { + elm.setAttribute('type', 'color'); + if (elm.type.toLowerCase() == 'color') { + return true; + } + } + return false; + })(), + + + dataProp : '_data_jscolor', + + + // usage: + // setData(obj, prop, value) + // setData(obj, {prop:value, ...}) + // + setData : function () { + var obj = arguments[0]; + + if (arguments.length === 3) { + // setting a single property + var data = obj.hasOwnProperty(jsc.dataProp) ? obj[jsc.dataProp] : (obj[jsc.dataProp] = {}); + var prop = arguments[1]; + var value = arguments[2]; + + data[prop] = value; + return true; + + } else if (arguments.length === 2 && typeof arguments[1] === 'object') { + // setting multiple properties + var data = obj.hasOwnProperty(jsc.dataProp) ? obj[jsc.dataProp] : (obj[jsc.dataProp] = {}); + var map = arguments[1]; + + for (var prop in map) { + if (map.hasOwnProperty(prop)) { + data[prop] = map[prop]; + } + } + return true; + } + + throw new Error('Invalid arguments'); + }, + + + // usage: + // removeData(obj, prop, [prop...]) + // + removeData : function () { + var obj = arguments[0]; + if (!obj.hasOwnProperty(jsc.dataProp)) { + return true; // data object does not exist + } + for (var i = 1; i < arguments.length; i += 1) { + var prop = arguments[i]; + delete obj[jsc.dataProp][prop]; + } + return true; + }, + + + getData : function (obj, prop, setDefault) { + if (!obj.hasOwnProperty(jsc.dataProp)) { + // data object does not exist + if (setDefault !== undefined) { + obj[jsc.dataProp] = {}; // create data object + } else { + return undefined; // no value to return + } + } + var data = obj[jsc.dataProp]; + + if (!data.hasOwnProperty(prop) && setDefault !== undefined) { + data[prop] = setDefault; + } + return data[prop]; + }, + + + getDataAttr : function (el, name) { + var attrName = 'data-' + name; + var attrValue = el.getAttribute(attrName); + return attrValue; + }, + + + _attachedGroupEvents : {}, + + + attachGroupEvent : function (groupName, el, evnt, func) { + if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + jsc._attachedGroupEvents[groupName] = []; + } + jsc._attachedGroupEvents[groupName].push([el, evnt, func]); + el.addEventListener(evnt, func, false); + }, + + + detachGroupEvents : function (groupName) { + if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) { + var evt = jsc._attachedGroupEvents[groupName][i]; + evt[0].removeEventListener(evt[1], evt[2], false); + } + delete jsc._attachedGroupEvents[groupName]; + } + }, + + + preventDefault : function (e) { + if (e.preventDefault) { e.preventDefault(); } + e.returnValue = false; + }, + + + captureTarget : function (target) { + // IE + if (target.setCapture) { + jsc._capturedTarget = target; + jsc._capturedTarget.setCapture(); + } + }, + + + releaseTarget : function () { + // IE + if (jsc._capturedTarget) { + jsc._capturedTarget.releaseCapture(); + jsc._capturedTarget = null; + } + }, + + + triggerEvent : function (el, eventName, bubbles, cancelable) { + if (!el) { + return; + } + + var ev = null; + + if (typeof Event === 'function') { + ev = new Event(eventName, { + bubbles: bubbles, + cancelable: cancelable + }); + } else { + // IE + ev = document.createEvent('Event'); + ev.initEvent(eventName, bubbles, cancelable); + } + + if (!ev) { + return false; + } + + // so that we know that the event was triggered internally + jsc.setData(ev, 'internal', true); + + el.dispatchEvent(ev); + return true; + }, + + + triggerInputEvent : function (el, eventName, bubbles, cancelable) { + if (!el) { + return; + } + if (jsc.isTextInput(el)) { + jsc.triggerEvent(el, eventName, bubbles, cancelable); + } + }, + + + eventKey : function (ev) { + var keys = { + 9: 'Tab', + 13: 'Enter', + 27: 'Escape', + }; + if (typeof ev.code === 'string') { + return ev.code; + } else if (ev.keyCode !== undefined && keys.hasOwnProperty(ev.keyCode)) { + return keys[ev.keyCode]; + } + return null; + }, + + + strList : function (str) { + if (!str) { + return []; + } + return str.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + + // The className parameter (str) can only contain a single class name + hasClass : function (elm, className) { + if (!className) { + return false; + } + if (elm.classList !== undefined) { + return elm.classList.contains(className); + } + // polyfill + return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' '); + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + addClass : function (elm, className) { + var classNames = jsc.strList(className); + + if (elm.classList !== undefined) { + for (var i = 0; i < classNames.length; i += 1) { + elm.classList.add(classNames[i]); + } + return; + } + // polyfill + for (var i = 0; i < classNames.length; i += 1) { + if (!jsc.hasClass(elm, classNames[i])) { + elm.className += (elm.className ? ' ' : '') + classNames[i]; + } + } + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + removeClass : function (elm, className) { + var classNames = jsc.strList(className); + + if (elm.classList !== undefined) { + for (var i = 0; i < classNames.length; i += 1) { + elm.classList.remove(classNames[i]); + } + return; + } + // polyfill + for (var i = 0; i < classNames.length; i += 1) { + var repl = new RegExp( + '^\\s*' + classNames[i] + '\\s*|' + + '\\s*' + classNames[i] + '\\s*$|' + + '\\s+' + classNames[i] + '(\\s+)', + 'g' + ); + elm.className = elm.className.replace(repl, '$1'); + } + }, + + + getCompStyle : function (elm) { + var compStyle = window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle; + + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned value is non-empty + if (!compStyle) { + return {}; + } + return compStyle; + }, + + + // Note: + // Setting a property to NULL reverts it to the state before it was first set + // with the 'reversible' flag enabled + // + setStyle : function (elm, styles, important, reversible) { + // using '' for standard priority (IE10 apparently doesn't like value undefined) + var priority = important ? 'important' : ''; + var origStyle = null; + + for (var prop in styles) { + if (styles.hasOwnProperty(prop)) { + var setVal = null; + + if (styles[prop] === null) { + // reverting a property value + + if (!origStyle) { + // get the original style object, but dont't try to create it if it doesn't exist + origStyle = jsc.getData(elm, 'origStyle'); + } + if (origStyle && origStyle.hasOwnProperty(prop)) { + // we have property's original value -> use it + setVal = origStyle[prop]; + } + + } else { + // setting a property value + + if (reversible) { + if (!origStyle) { + // get the original style object and if it doesn't exist, create it + origStyle = jsc.getData(elm, 'origStyle', {}); + } + if (!origStyle.hasOwnProperty(prop)) { + // original property value not yet stored -> store it + origStyle[prop] = elm.style[prop]; + } + } + setVal = styles[prop]; + } + + if (setVal !== null) { + elm.style.setProperty(prop, setVal, priority); + } + } + } + }, + + + linearGradient : (function () { + + function getFuncName () { + var stdName = 'linear-gradient'; + var prefixes = ['', '-webkit-', '-moz-', '-o-', '-ms-']; + var helper = document.createElement('div'); + + for (var i = 0; i < prefixes.length; i += 1) { + var tryFunc = prefixes[i] + stdName; + var tryVal = tryFunc + '(to right, rgba(0,0,0,0), rgba(0,0,0,0))'; + + helper.style.background = tryVal; + if (helper.style.background) { // CSS background successfully set -> function name is supported + return tryFunc; + } + } + return stdName; // fallback to standard 'linear-gradient' without vendor prefix + } + + var funcName = getFuncName(); + + return function () { + return funcName + '(' + Array.prototype.join.call(arguments, ', ') + ')'; + }; + + })(), + + + setBorderRadius : function (elm, value) { + jsc.setStyle(elm, {'border-radius' : value || '0'}); + }, + + + setBoxShadow : function (elm, value) { + jsc.setStyle(elm, {'box-shadow': value || 'none'}); + }, + + + getElementPos : function (e, relativeToViewport) { + var x=0, y=0; + var rect = e.getBoundingClientRect(); + x = rect.left; + y = rect.top; + if (!relativeToViewport) { + var viewPos = jsc.getViewPos(); + x += viewPos[0]; + y += viewPos[1]; + } + return [x, y]; + }, + + + getElementSize : function (e) { + return [e.offsetWidth, e.offsetHeight]; + }, + + + // get pointer's X/Y coordinates relative to viewport + getAbsPointerPos : function (e) { + var x = 0, y = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + x = e.changedTouches[0].clientX; + y = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + x = e.clientX; + y = e.clientY; + } + return { x: x, y: y }; + }, + + + // get pointer's X/Y coordinates relative to target element + getRelPointerPos : function (e) { + var target = e.target || e.srcElement; + var targetRect = target.getBoundingClientRect(); + + var x = 0, y = 0; + + var clientX = 0, clientY = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + clientX = e.changedTouches[0].clientX; + clientY = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + clientX = e.clientX; + clientY = e.clientY; + } + + x = clientX - targetRect.left; + y = clientY - targetRect.top; + return { x: x, y: y }; + }, + + + getViewPos : function () { + var doc = document.documentElement; + return [ + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) + ]; + }, + + + getViewSize : function () { + var doc = document.documentElement; + return [ + (window.innerWidth || doc.clientWidth), + (window.innerHeight || doc.clientHeight), + ]; + }, + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + // returns: [ 0-360, 0-100, 0-100 ] + // + RGB_HSV : function (r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if (m === 0) { return [ null, 0, 100 * v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ + 60 * (h===6?0:h), + 100 * (m/v), + 100 * v + ]; + }, + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + // returns: [ 0-255, 0-255, 0-255 ] + // + HSV_RGB : function (h, s, v) { + var u = 255 * (v / 100); + + if (h === null) { + return [ u, u, u ]; + } + + h /= 60; + s /= 100; + + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = u * (1 - s); + var n = u * (1 - s * f); + switch (i) { + case 6: + case 0: return [u,n,m]; + case 1: return [n,u,m]; + case 2: return [m,u,n]; + case 3: return [m,n,u]; + case 4: return [n,m,u]; + case 5: return [u,m,n]; + } + }, + + + parseColorString : function (str) { + var ret = { + rgba: null, + format: null // 'hex' | 'rgb' | 'rgba' + }; + + var m; + if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) { + // HEX notation + + ret.format = 'hex'; + + if (m[1].length === 6) { + // 6-char notation + ret.rgba = [ + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16), + null + ]; + } else { + // 3-char notation + ret.rgba = [ + parseInt(m[1].charAt(0) + m[1].charAt(0),16), + parseInt(m[1].charAt(1) + m[1].charAt(1),16), + parseInt(m[1].charAt(2) + m[1].charAt(2),16), + null + ]; + } + return ret; + + } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) { + // rgb(...) or rgba(...) notation + + var params = m[1].split(','); + var re = /^\s*(\d+|\d*\.\d+|\d+\.\d*)\s*$/; + var mR, mG, mB, mA; + if ( + params.length >= 3 && + (mR = params[0].match(re)) && + (mG = params[1].match(re)) && + (mB = params[2].match(re)) + ) { + ret.format = 'rgb'; + ret.rgba = [ + parseFloat(mR[1]) || 0, + parseFloat(mG[1]) || 0, + parseFloat(mB[1]) || 0, + null + ]; + + if ( + params.length >= 4 && + (mA = params[3].match(re)) + ) { + ret.format = 'rgba'; + ret.rgba[3] = parseFloat(mA[1]) || 0; + } + return ret; + } + } + + return false; + }, + + + // Canvas scaling for retina displays + // + // adapted from https://www.html5rocks.com/en/tutorials/canvas/hidpi/ + // + scaleCanvasForHighDPR : function (canvas) { + var dpr = window.devicePixelRatio || 1; + canvas.width *= dpr; + canvas.height *= dpr; + var ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + }, + + + genColorPreviewCanvas : function (color, separatorPos, specWidth, scaleForHighDPR) { + + var sepW = Math.round(jsc.pub.previewSeparator.length); + var sqSize = jsc.pub.chessboardSize; + var sqColor1 = jsc.pub.chessboardColor1; + var sqColor2 = jsc.pub.chessboardColor2; + + var cWidth = specWidth ? specWidth : sqSize * 2; + var cHeight = sqSize * 2; + + var canvas = jsc.createEl('canvas'); + var ctx = canvas.getContext('2d'); + + canvas.width = cWidth; + canvas.height = cHeight; + if (scaleForHighDPR) { + jsc.scaleCanvasForHighDPR(canvas); + } + + // transparency chessboard - background + ctx.fillStyle = sqColor1; + ctx.fillRect(0, 0, cWidth, cHeight); + + // transparency chessboard - squares + ctx.fillStyle = sqColor2; + for (var x = 0; x < cWidth; x += sqSize * 2) { + ctx.fillRect(x, 0, sqSize, sqSize); + ctx.fillRect(x + sqSize, sqSize, sqSize, sqSize); + } + + if (color) { + // actual color in foreground + ctx.fillStyle = color; + ctx.fillRect(0, 0, cWidth, cHeight); + } + + var start = null; + switch (separatorPos) { + case 'left': + start = 0; + ctx.clearRect(0, 0, sepW/2, cHeight); + break; + case 'right': + start = cWidth - sepW; + ctx.clearRect(cWidth - (sepW/2), 0, sepW/2, cHeight); + break; + } + if (start !== null) { + ctx.lineWidth = 1; + for (var i = 0; i < jsc.pub.previewSeparator.length; i += 1) { + ctx.beginPath(); + ctx.strokeStyle = jsc.pub.previewSeparator[i]; + ctx.moveTo(0.5 + start + i, 0); + ctx.lineTo(0.5 + start + i, cHeight); + ctx.stroke(); + } + } + + return { + canvas: canvas, + width: cWidth, + height: cHeight, + }; + }, + + + // if position or width is not set => fill the entire element (0%-100%) + genColorPreviewGradient : function (color, position, width) { + var params = []; + + if (position && width) { + params = [ + 'to ' + {'left':'right', 'right':'left'}[position], + color + ' 0%', + color + ' ' + width + 'px', + 'rgba(0,0,0,0) ' + (width + 1) + 'px', + 'rgba(0,0,0,0) 100%', + ]; + } else { + params = [ + 'to right', + color + ' 0%', + color + ' 100%', + ]; + } + + return jsc.linearGradient.apply(this, params); + }, + + + redrawPosition : function () { + + if (jsc.picker && jsc.picker.owner) { + var thisObj = jsc.picker.owner; + + var tp, vp; + + if (thisObj.fixed) { + // Fixed elements are positioned relative to viewport, + // therefore we can ignore the scroll offset + tp = jsc.getElementPos(thisObj.targetElement, true); // target pos + vp = [0, 0]; // view pos + } else { + tp = jsc.getElementPos(thisObj.targetElement); // target pos + vp = jsc.getViewPos(); // view pos + } + + var ts = jsc.getElementSize(thisObj.targetElement); // target size + var vs = jsc.getViewSize(); // view size + var ps = jsc.getPickerOuterDims(thisObj); // picker size + var a, b, c; + switch (thisObj.position.toLowerCase()) { + case 'left': a=1; b=0; c=-1; break; + case 'right':a=1; b=0; c=1; break; + case 'top': a=0; b=1; c=-1; break; + default: a=0; b=1; c=1; break; + } + var l = (ts[b]+ps[b])/2; + + // compute picker position + if (!thisObj.smartPosition) { + var pp = [ + tp[a], + tp[b]+ts[b]-l+l*c + ]; + } else { + var pp = [ + -vp[a]+tp[a]+ps[a] > vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + + var x = pp[a]; + var y = pp[b]; + var positionValue = thisObj.fixed ? 'fixed' : 'absolute'; + var contractShadow = + (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) && + (pp[1] + ps[1] < tp[1] + ts[1]); + + jsc._drawPosition(thisObj, x, y, positionValue, contractShadow); + } + }, + + + _drawPosition : function (thisObj, x, y, positionValue, contractShadow) { + var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px + + jsc.picker.wrap.style.position = positionValue; + jsc.picker.wrap.style.left = x + 'px'; + jsc.picker.wrap.style.top = y + 'px'; + + jsc.setBoxShadow( + jsc.picker.boxS, + thisObj.shadow ? + new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) : + null); + }, + + + getPickerDims : function (thisObj) { + var dims = [ + 2 * thisObj.controlBorderWidth + 2 * thisObj.padding + thisObj.width, + 2 * thisObj.controlBorderWidth + 2 * thisObj.padding + thisObj.height + ]; + var sliderSpace = 2 * thisObj.controlBorderWidth + 2 * jsc.getControlPadding(thisObj) + thisObj.sliderSize; + if (jsc.getSliderChannel(thisObj)) { + dims[0] += sliderSpace; + } + if (thisObj.hasAlphaChannel()) { + dims[0] += sliderSpace; + } + if (thisObj.closeButton) { + dims[1] += 2 * thisObj.controlBorderWidth + thisObj.padding + thisObj.buttonHeight; + } + return dims; + }, + + + getPickerOuterDims : function (thisObj) { + var dims = jsc.getPickerDims(thisObj); + return [ + dims[0] + 2 * thisObj.borderWidth, + dims[1] + 2 * thisObj.borderWidth + ]; + }, + + + getControlPadding : function (thisObj) { + return Math.max( + thisObj.padding / 2, + (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness) - thisObj.controlBorderWidth + ); + }, + + + getPadYChannel : function (thisObj) { + switch (thisObj.mode.charAt(1).toLowerCase()) { + case 'v': return 'v'; break; + } + return 's'; + }, + + + getSliderChannel : function (thisObj) { + if (thisObj.mode.length > 2) { + switch (thisObj.mode.charAt(2).toLowerCase()) { + case 's': return 's'; break; + case 'v': return 'v'; break; + } + } + return null; + }, + + + onDocumentMouseDown : function (e) { + var target = e.target || e.srcElement; + + if (target.jscolor && target.jscolor instanceof jsc.pub) { // clicked targetElement -> show picker + if (target.jscolor.showOnClick && !target.disabled) { + target.jscolor.show(); + } + } else if (jsc.getData(target, 'gui')) { // clicked jscolor's GUI element + var control = jsc.getData(target, 'control'); + if (control) { + // jscolor's control + jsc.onControlPointerStart(e, target, jsc.getData(target, 'control'), 'mouse'); + } + } else { + // mouse is outside the picker's controls -> hide the color picker! + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.tryHide(); + } + } + }, + + + onDocumentKeyUp : function (e) { + if (['Tab', 'Escape'].indexOf(jsc.eventKey(e)) !== -1) { + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.tryHide(); + } + } + }, + + + onWindowResize : function (e) { + jsc.redrawPosition(); + }, + + + onParentScroll : function (e) { + // hide the picker when one of the parent elements is scrolled + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.tryHide(); + } + }, + + + onPickerTouchStart : function (e) { + var target = e.target || e.srcElement; + + if (jsc.getData(target, 'control')) { + jsc.onControlPointerStart(e, target, jsc.getData(target, 'control'), 'touch'); + } + }, + + + // calls function specified in picker's property + triggerCallback : function (thisObj, prop) { + if (!thisObj[prop]) { + return; // callback func not specified + } + var callback = null; + + if (typeof thisObj[prop] === 'string') { + // string with code + try { + callback = new Function (thisObj[prop]); + } catch (e) { + console.error(e); + } + } else { + // function + callback = thisObj[prop]; + } + + if (callback) { + callback.call(thisObj); + } + }, + + + // Triggers a color change related event(s) on all picker instances. + // It is possible to specify multiple events separated with a space. + triggerGlobal : function (eventNames) { + var inst = jsc.getInstances(); + for (var i = 0; i < inst.length; i += 1) { + inst[i].trigger(eventNames); + } + }, + + + _pointerMoveEvent : { + mouse: 'mousemove', + touch: 'touchmove' + }, + _pointerEndEvent : { + mouse: 'mouseup', + touch: 'touchend' + }, + + + _pointerOrigin : null, + _capturedTarget : null, + + + onControlPointerStart : function (e, target, controlName, pointerType) { + var thisObj = jsc.getData(target, 'instance'); + + jsc.preventDefault(e); + jsc.captureTarget(target); + + var registerDragEvents = function (doc, offset) { + jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType], + jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset)); + jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType], + jsc.onDocumentPointerEnd(e, target, controlName, pointerType)); + }; + + registerDragEvents(document, [0, 0]); + + if (window.parent && window.frameElement) { + var rect = window.frameElement.getBoundingClientRect(); + var ofs = [-rect.left, -rect.top]; + registerDragEvents(window.parent.window.document, ofs); + } + + var abs = jsc.getAbsPointerPos(e); + var rel = jsc.getRelPointerPos(e); + jsc._pointerOrigin = { + x: abs.x - rel.x, + y: abs.y - rel.y + }; + + switch (controlName) { + case 'pad': + // if the value slider is at the bottom, move it up + if (jsc.getSliderChannel(thisObj) === 'v' && thisObj.channels.v === 0) { + thisObj.fromHSVA(null, null, 100, null); + } + jsc.setPad(thisObj, e, 0, 0); + break; + + case 'sld': + jsc.setSld(thisObj, e, 0); + break; + + case 'asld': + jsc.setASld(thisObj, e, 0); + break; + } + thisObj.trigger('input'); + }, + + + onDocumentPointerMove : function (e, target, controlName, pointerType, offset) { + return function (e) { + var thisObj = jsc.getData(target, 'instance'); + switch (controlName) { + case 'pad': + jsc.setPad(thisObj, e, offset[0], offset[1]); + break; + + case 'sld': + jsc.setSld(thisObj, e, offset[1]); + break; + + case 'asld': + jsc.setASld(thisObj, e, offset[1]); + break; + } + thisObj.trigger('input'); + } + }, + + + onDocumentPointerEnd : function (e, target, controlName, pointerType) { + return function (e) { + var thisObj = jsc.getData(target, 'instance'); + jsc.detachGroupEvents('drag'); + jsc.releaseTarget(); + + // Always trigger changes AFTER detaching outstanding mouse handlers, + // in case some color change occured in user-defined onChange/onInput handler + // would intrude into current mouse events + thisObj.trigger('input'); + thisObj.trigger('change'); + }; + }, + + + setPad : function (thisObj, e, ofsX, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.controlBorderWidth; + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.controlBorderWidth; + + var xVal = x * (360 / (thisObj.width - 1)); + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getPadYChannel(thisObj)) { + case 's': thisObj.fromHSVA(xVal, yVal, null, null); break; + case 'v': thisObj.fromHSVA(xVal, null, yVal, null); break; + } + }, + + + setSld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.controlBorderWidth; + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getSliderChannel(thisObj)) { + case 's': thisObj.fromHSVA(null, yVal, null, null); break; + case 'v': thisObj.fromHSVA(null, null, yVal, null); break; + } + }, + + + setASld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.controlBorderWidth; + var yVal = 1.0 - (y * (1.0 / (thisObj.height - 1))); + + if (yVal < 1.0) { + // if format is flexible and the current format doesn't support alpha, switch to a suitable one + if (thisObj.format.toLowerCase() === 'any' && thisObj.getFormat() !== 'rgba') { + thisObj._currentFormat = 'rgba'; + } + } + + thisObj.fromHSVA(null, null, null, yVal); + }, + + + createPalette : function () { + + var paletteObj = { + elm: null, + draw: null + }; + + var canvas = jsc.createEl('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, type) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0); + hGrad.addColorStop(0 / 6, '#F00'); + hGrad.addColorStop(1 / 6, '#FF0'); + hGrad.addColorStop(2 / 6, '#0F0'); + hGrad.addColorStop(3 / 6, '#0FF'); + hGrad.addColorStop(4 / 6, '#00F'); + hGrad.addColorStop(5 / 6, '#F0F'); + hGrad.addColorStop(6 / 6, '#F00'); + + ctx.fillStyle = hGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height); + switch (type.toLowerCase()) { + case 's': + vGrad.addColorStop(0, 'rgba(255,255,255,0)'); + vGrad.addColorStop(1, 'rgba(255,255,255,1)'); + break; + case 'v': + vGrad.addColorStop(0, 'rgba(0,0,0,0)'); + vGrad.addColorStop(1, 'rgba(0,0,0,1)'); + break; + } + ctx.fillStyle = vGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + paletteObj.elm = canvas; + paletteObj.draw = drawFunc; + + return paletteObj; + }, + + + createSliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + var canvas = jsc.createEl('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color1, color2) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + return sliderObj; + }, + + + createASliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + var canvas = jsc.createEl('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var sqSize = canvas.width / 2; + var sqColor1 = jsc.pub.chessboardColor1; + var sqColor2 = jsc.pub.chessboardColor2; + + // dark gray background + ctx.fillStyle = sqColor1; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + for (var y = 0; y < canvas.height; y += sqSize * 2) { + // light gray squares + ctx.fillStyle = sqColor2; + ctx.fillRect(0, y, sqSize, sqSize); + ctx.fillRect(sqSize, y + sqSize, sqSize, sqSize); + } + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color); + grad.addColorStop(1, 'rgba(0,0,0,0)'); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + return sliderObj; + }, + + + BoxShadow : (function () { + var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) { + this.hShadow = hShadow; + this.vShadow = vShadow; + this.blur = blur; + this.spread = spread; + this.color = color; + this.inset = !!inset; + }; + + BoxShadow.prototype.toString = function () { + var vals = [ + Math.round(this.hShadow) + 'px', + Math.round(this.vShadow) + 'px', + Math.round(this.blur) + 'px', + Math.round(this.spread) + 'px', + this.color + ]; + if (this.inset) { + vals.push('inset'); + } + return vals.join(' '); + }; + + return BoxShadow; + })(), + + + flags : { + leaveValue : 1 << 0, + leaveAlpha : 1 << 1, + leavePreview : 1 << 2, + }, + + + enumOpts : { + format: ['auto', 'any', 'hex', 'rgb', 'rgba'], + previewPosition: ['left', 'right'], + mode: ['hsv', 'hvs', 'hs', 'hv'], + position: ['left', 'right', 'top', 'bottom'], + alphaChannel: ['auto', true, false], + }, + + + deprecatedOpts : { + // : ( can be null) + 'styleElement': 'previewElement', + 'onFineChange': 'onInput', + 'overwriteImportant': 'forceStyle', + 'closable': 'closeButton', + 'insetWidth': 'controlBorderWidth', + 'insetColor': 'controlBorderColor', + 'refine': null, + }, + + + docsRef : ' ' + 'See https://jscolor.com/docs/', + + + // + // Usage: + // var myPicker = new JSColor( [, ]) + // + // (constructor is accessible via both 'jscolor' and 'JSColor' name) + // + + pub : function (targetElement, opts) { + + var THIS = this; + + if (!opts) { + opts = {}; + } + + this.channels = { + r: 255, // red [0-255] + g: 255, // green [0-255] + b: 255, // blue [0-255] + h: 0, // hue [0-360] + s: 0, // saturation [0-100] + v: 100, // value (brightness) [0-100] + a: 1.0, // alpha (opacity) [0.0 - 1.0] + }; + + // General options + // + this.format = 'auto'; // 'auto' | 'any' | 'hex' | 'rgb' | 'rgba' - Format of the input/output value + this.value = undefined; // INITIAL color value in any supported format. To change it later, use method fromString(), fromHSVA(), fromRGBA() or channel() + this.alpha = undefined; // INITIAL alpha value. To change it later, call method channel('A', ) + this.onChange = undefined; // called when color changes. Value can be either a function or a string with JS code. + this.onInput = undefined; // called repeatedly as the color is being changed, e.g. while dragging a slider. Value can be either a function or a string with JS code. + this.valueElement = undefined; // element that will be used to display and input the color value + this.alphaElement = undefined; // element that will be used to display and input the alpha (opacity) value + this.previewElement = undefined; // element that will preview the picked color using CSS background + this.previewPosition = 'left'; // 'left' | 'right' - position of the color preview in previewElement + this.previewSize = 32; // (px) width of the color preview displayed in previewElement + this.previewPadding = 8; // (px) space between color preview and content of the previewElement + this.required = true; // whether the associated text input must always contain a color value. If false, the input can be left empty. + this.hash = true; // whether to prefix the HEX color code with # symbol (only applicable for HEX format) + this.uppercase = true; // whether to show the HEX color code in upper case (only applicable for HEX format) + this.forceStyle = true; // whether to overwrite CSS style of the previewElement using !important flag + + // Color Picker options + // + this.width = 181; // width of color palette (in px) + this.height = 101; // height of color palette (in px) + this.mode = 'HSV'; // 'HSV' | 'HVS' | 'HS' | 'HV' - layout of the color picker controls + this.alphaChannel = 'auto'; // 'auto' | true | false - if alpha channel is enabled, the alpha slider will be visible. If 'auto', it will be determined according to color format + this.position = 'bottom'; // 'left' | 'right' | 'top' | 'bottom' - position relative to the target element + this.smartPosition = true; // automatically change picker position when there is not enough space for it + this.showOnClick = true; // whether to show the picker when user clicks its target element + this.hideOnLeave = true; // whether to automatically hide the picker when user leaves its target element (e.g. upon clicking the document) + this.sliderSize = 16; // px + this.crossSize = 8; // px + this.closeButton = false; // whether to display the Close button + this.closeText = 'Close'; + this.buttonColor = 'rgba(0,0,0,1)'; // CSS color + this.buttonHeight = 18; // px + this.padding = 12; // px + this.backgroundColor = 'rgba(255,255,255,1)'; // CSS color + this.borderWidth = 1; // px + this.borderColor = 'rgba(187,187,187,1)'; // CSS color + this.borderRadius = 8; // px + this.controlBorderWidth = 1; // px + this.controlBorderColor = 'rgba(187,187,187,1)'; // CSS color + this.shadow = true; // whether to display a shadow + this.shadowBlur = 15; // px + this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color + this.pointerColor = 'rgba(76,76,76,1)'; // CSS color + this.pointerBorderWidth = 1; // px + this.pointerBorderColor = 'rgba(255,255,255,1)'; // CSS color + this.pointerThickness = 2; // px + this.zIndex = 5000; + this.container = undefined; // where to append the color picker (BODY element by default) + + // Experimental + // + this.minS = 0; // min allowed saturation (0 - 100) + this.maxS = 100; // max allowed saturation (0 - 100) + this.minV = 0; // min allowed value (brightness) (0 - 100) + this.maxV = 100; // max allowed value (brightness) (0 - 100) + this.minA = 0.0; // min allowed alpha (opacity) (0.0 - 1.0) + this.maxA = 1.0; // max allowed alpha (opacity) (0.0 - 1.0) + + + // let's process the DEPRECATED 'options' property (this will be later removed) + if (jsc.pub.options) { + // let's set custom default options, if specified + for (var opt in jsc.pub.options) { + if (jsc.pub.options.hasOwnProperty(opt)) { + try { + setOption(opt, jsc.pub.options[opt]); + } catch (e) { + console.warn(e); + } + } + } + } + + + // let's apply configuration presets + // + var presetsArr = []; + + if (opts.preset) { + if (typeof opts.preset === 'string') { + presetsArr = opts.preset.split(/\s+/); + } else if (Array.isArray(opts.preset)) { + presetsArr = opts.preset.slice(); // slice() to clone + } else { + console.warn('Unrecognized preset value'); + } + } + + // always use the 'default' preset. If it's not listed, append it to the end. + if (presetsArr.indexOf('default') === -1) { + presetsArr.push('default'); + } + + // let's apply the presets in reverse order, so that should there be any overlapping options, + // the formerly listed preset will override the latter + for (var i = presetsArr.length - 1; i >= 0; i -= 1) { + var pres = presetsArr[i]; + if (!pres) { + continue; // preset is empty string + } + if (!jsc.pub.presets.hasOwnProperty(pres)) { + console.warn('Unknown preset: %s', pres); + continue; + } + for (var opt in jsc.pub.presets[pres]) { + if (jsc.pub.presets[pres].hasOwnProperty(opt)) { + try { + setOption(opt, jsc.pub.presets[pres][opt]); + } catch (e) { + console.warn(e); + } + } + } + } + + + // let's set specific options for this color picker + var nonProperties = [ + // these options won't be set as instance properties + 'preset', + ]; + for (var opt in opts) { + if (opts.hasOwnProperty(opt)) { + if (nonProperties.indexOf(opt) === -1) { + try { + setOption(opt, opts[opt]); + } catch (e) { + console.warn(e); + } + } + } + } + + + // Getter: option(name) + // Setter: option(name, value) + // option({name:value, ...}) + // + this.option = function () { + if (!arguments.length) { + throw new Error('No option specified'); + } + + if (arguments.length === 1 && typeof arguments[0] === 'string') { + // getting a single option + try { + return getOption(arguments[0]); + } catch (e) { + console.warn(e); + } + return false; + + } else if (arguments.length >= 2 && typeof arguments[0] === 'string') { + // setting a single option + try { + if (!setOption(arguments[0], arguments[1])) { + return false; + } + } catch (e) { + console.warn(e); + return false; + } + this.redraw(); // immediately redraws the picker, if it's displayed + this.exposeColor(); // in case some preview-related or format-related option was changed + return true; + + } else if (arguments.length === 1 && typeof arguments[0] === 'object') { + // setting multiple options + var opts = arguments[0]; + var success = true; + for (var opt in opts) { + if (opts.hasOwnProperty(opt)) { + try { + if (!setOption(opt, opts[opt])) { + success = false; + } + } catch (e) { + console.warn(e); + success = false; + } + } + } + this.redraw(); // immediately redraws the picker, if it's displayed + this.exposeColor(); // in case some preview-related or format-related option was changed + return success; + } + + throw new Error('Invalid arguments'); + } + + + // Getter: channel(name) + // Setter: channel(name, value) + // + this.channel = function (name, value) { + if (typeof name !== 'string') { + throw new Error('Invalid value for channel name: ' + name); + } + + if (value === undefined) { + // getting channel value + if (!this.channels.hasOwnProperty(name.toLowerCase())) { + console.warn('Getting unknown channel: ' + name); + return false; + } + return this.channels[name.toLowerCase()]; + + } else { + // setting channel value + var res = false; + switch (name.toLowerCase()) { + case 'r': res = this.fromRGBA(value, null, null, null); break; + case 'g': res = this.fromRGBA(null, value, null, null); break; + case 'b': res = this.fromRGBA(null, null, value, null); break; + case 'h': res = this.fromHSVA(value, null, null, null); break; + case 's': res = this.fromHSVA(null, value, null, null); break; + case 'v': res = this.fromHSVA(null, null, value, null); break; + case 'a': res = this.fromHSVA(null, null, null, value); break; + default: + console.warn('Setting unknown channel: ' + name); + return false; + } + if (res) { + this.redraw(); // immediately redraws the picker, if it's displayed + return true; + } + } + + return false; + } + + + // Triggers given input event(s) by: + // - executing on callback specified as picker's option + // - triggering standard DOM event listeners attached to the value element + // + // It is possible to specify multiple events separated with a space. + // + this.trigger = function (eventNames) { + var evs = jsc.strList(eventNames); + for (var i = 0; i < evs.length; i += 1) { + var ev = evs[i].toLowerCase(); + + // trigger a callback + var callbackProp = null; + switch (ev) { + case 'input': callbackProp = 'onInput'; break; + case 'change': callbackProp = 'onChange'; break; + } + if (callbackProp) { + jsc.triggerCallback(this, callbackProp); + } + + // trigger standard DOM event listeners on the value element + jsc.triggerInputEvent(this.valueElement, ev, true, true); + } + }; + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // a: 0.0-1.0 + // + this.fromHSVA = function (h, s, v, a, flags) { // null = don't change + if (h === undefined) { h = null; } + if (s === undefined) { s = null; } + if (v === undefined) { v = null; } + if (a === undefined) { a = null; } + + if (h !== null) { + if (isNaN(h)) { return false; } + this.channels.h = Math.max(0, Math.min(360, h)); + } + if (s !== null) { + if (isNaN(s)) { return false; } + this.channels.s = Math.max(0, Math.min(100, this.maxS, s), this.minS); + } + if (v !== null) { + if (isNaN(v)) { return false; } + this.channels.v = Math.max(0, Math.min(100, this.maxV, v), this.minV); + } + if (a !== null) { + if (isNaN(a)) { return false; } + this.channels.a = this.hasAlphaChannel() ? + Math.max(0, Math.min(1, this.maxA, a), this.minA) : + 1.0; // if alpha channel is disabled, the color should stay 100% opaque + } + + var rgb = jsc.HSV_RGB( + this.channels.h, + this.channels.s, + this.channels.v + ); + this.channels.r = rgb[0]; + this.channels.g = rgb[1]; + this.channels.b = rgb[2]; + + this.exposeColor(flags); + return true; + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // a: 0.0-1.0 + // + this.fromRGBA = function (r, g, b, a, flags) { // null = don't change + if (r === undefined) { r = null; } + if (g === undefined) { g = null; } + if (b === undefined) { b = null; } + if (a === undefined) { a = null; } + + if (r !== null) { + if (isNaN(r)) { return false; } + r = Math.max(0, Math.min(255, r)); + } + if (g !== null) { + if (isNaN(g)) { return false; } + g = Math.max(0, Math.min(255, g)); + } + if (b !== null) { + if (isNaN(b)) { return false; } + b = Math.max(0, Math.min(255, b)); + } + if (a !== null) { + if (isNaN(a)) { return false; } + this.channels.a = this.hasAlphaChannel() ? + Math.max(0, Math.min(1, this.maxA, a), this.minA) : + 1.0; // if alpha channel is disabled, the color should stay 100% opaque + } + + var hsv = jsc.RGB_HSV( + r===null ? this.channels.r : r, + g===null ? this.channels.g : g, + b===null ? this.channels.b : b + ); + if (hsv[0] !== null) { + this.channels.h = Math.max(0, Math.min(360, hsv[0])); + } + if (hsv[2] !== 0) { // fully black color stays black through entire saturation range, so let's not change saturation + this.channels.s = Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1])); + } + this.channels.v = Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = jsc.HSV_RGB(this.channels.h, this.channels.s, this.channels.v); + this.channels.r = rgb[0]; + this.channels.g = rgb[1]; + this.channels.b = rgb[2]; + + this.exposeColor(flags); + return true; + }; + + + // DEPRECATED. Use .fromHSVA() instead + // + this.fromHSV = function (h, s, v, flags) { + console.warn('fromHSV() method is DEPRECATED. Using fromHSVA() instead.' + jsc.docsRef); + return this.fromHSVA(h, s, v, null, flags); + }; + + + // DEPRECATED. Use .fromRGBA() instead + // + this.fromRGB = function (r, g, b, flags) { + console.warn('fromRGB() method is DEPRECATED. Using fromRGBA() instead.' + jsc.docsRef); + return this.fromRGBA(r, g, b, null, flags); + }; + + + this.fromString = function (str, flags) { + if (!this.required && str.trim() === '') { + // setting empty string to an optional color input + this.setPreviewElementBg(null); + this.setValueElementValue(''); + return true; + } + + var color = jsc.parseColorString(str); + if (!color) { + return false; // could not parse + } + if (this.format.toLowerCase() === 'any') { + this._currentFormat = color.format; // adapt format + if (this.getFormat() !== 'rgba') { + color.rgba[3] = 1.0; // when switching to a format that doesn't support alpha, set full opacity + } + this.redraw(); // to show/hide the alpha slider according to current format + } + this.fromRGBA( + color.rgba[0], + color.rgba[1], + color.rgba[2], + color.rgba[3], + flags + ); + return true; + }; + + + this.toString = function (format) { + if (format === undefined) { + format = this.getFormat(); // format not specified -> use the current format + } + switch (format.toLowerCase()) { + case 'hex': return this.toHEXString(); break; + case 'rgb': return this.toRGBString(); break; + case 'rgba': return this.toRGBAString(); break; + } + return false; + }; + + + this.toHEXString = function () { + return '#' + ( + ('0' + Math.round(this.channels.r).toString(16)).substr(-2) + + ('0' + Math.round(this.channels.g).toString(16)).substr(-2) + + ('0' + Math.round(this.channels.b).toString(16)).substr(-2) + ).toUpperCase(); + }; + + + this.toRGBString = function () { + return ('rgb(' + + Math.round(this.channels.r) + ',' + + Math.round(this.channels.g) + ',' + + Math.round(this.channels.b) + + ')'); + }; + + + this.toRGBAString = function () { + return ('rgba(' + + Math.round(this.channels.r) + ',' + + Math.round(this.channels.g) + ',' + + Math.round(this.channels.b) + ',' + + (Math.round(this.channels.a * 100) / 100) + + ')'); + }; + + + this.toGrayscale = function () { + return ( + 0.213 * this.channels.r + + 0.715 * this.channels.g + + 0.072 * this.channels.b + ); + }; + + + this.toCanvas = function () { + return jsc.genColorPreviewCanvas(this.toRGBAString()).canvas; + }; + + + this.toDataURL = function () { + return this.toCanvas().toDataURL(); + }; + + + this.toBackground = function () { + return jsc.pub.background(this.toRGBAString()); + }; + + + this.isLight = function () { + return this.toGrayscale() > 255 / 2; + }; + + + this.hide = function () { + if (isPickerOwner()) { + detachPicker(); + } + }; + + + this.show = function () { + drawPicker(); + }; + + + this.redraw = function () { + if (isPickerOwner()) { + drawPicker(); + } + }; + + + this.getFormat = function () { + return this._currentFormat; + }; + + + this.hasAlphaChannel = function () { + if (this.alphaChannel === 'auto') { + return ( + this.format.toLowerCase() === 'any' || // format can change on the fly (e.g. from hex to rgba), so let's consider the alpha channel enabled + this.getFormat() === 'rgba' || // the current format supports alpha channel + this.alpha !== undefined || // initial alpha value is set, so we're working with alpha channel + this.alphaElement !== undefined // the alpha value is redirected, so we're working with alpha channel + ); + } + + return this.alphaChannel; // the alpha channel is explicitly set + }; + + + this.processValueInput = function (str) { + if (!this.fromString(str)) { + // could not parse the color value - let's just expose the current color + this.exposeColor(); + } + }; + + + this.processAlphaInput = function (str) { + if (!this.fromHSVA(null, null, null, parseFloat(str))) { + // could not parse the alpha value - let's just expose the current color + this.exposeColor(); + } + }; + + + this.exposeColor = function (flags) { + + if (!(flags & jsc.flags.leaveValue) && this.valueElement) { + var value = this.toString(); + + if (this.getFormat() === 'hex') { + if (!this.uppercase) { value = value.toLowerCase(); } + if (!this.hash) { value = value.replace(/^#/, ''); } + } + + this.setValueElementValue(value); + } + + if (!(flags & jsc.flags.leaveAlpha) && this.alphaElement) { + var value = Math.round(this.channels.a * 100) / 100; + this.setAlphaElementValue(value); + } + + if (!(flags & jsc.flags.leavePreview) && this.previewElement) { + var previewPos = null; // 'left' | 'right' (null -> fill the entire element) + + if ( + jsc.isTextInput(this.previewElement) || // text input + (jsc.isButton(this.previewElement) && !jsc.isButtonEmpty(this.previewElement)) // button with text + ) { + previewPos = this.previewPosition; + } + + this.setPreviewElementBg(this.toRGBAString()); + } + + if (isPickerOwner()) { + redrawPad(); + redrawSld(); + redrawASld(); + } + }; + + + this.setPreviewElementBg = function (color) { + if (!this.previewElement) { + return; + } + + var position = null; // color preview position: null | 'left' | 'right' + var width = null; // color preview width: px | null = fill the entire element + if ( + jsc.isTextInput(this.previewElement) || // text input + (jsc.isButton(this.previewElement) && !jsc.isButtonEmpty(this.previewElement)) // button with text + ) { + position = this.previewPosition; + width = this.previewSize; + } + + var backgrounds = []; + + if (!color) { + // there is no color preview to display -> let's remove any previous background image + backgrounds.push({ + image: 'none', + position: 'left top', + size: 'auto', + repeat: 'no-repeat', + origin: 'padding-box', + }); + } else { + // CSS gradient for background color preview + backgrounds.push({ + image: jsc.genColorPreviewGradient( + color, + position, + width ? width - jsc.pub.previewSeparator.length : null + ), + position: 'left top', + size: 'auto', + repeat: position ? 'repeat-y' : 'repeat', + origin: 'padding-box', + }); + + // data URL of generated PNG image with a gray transparency chessboard + var preview = jsc.genColorPreviewCanvas( + 'rgba(0,0,0,0)', + position ? {'left':'right', 'right':'left'}[position] : null, + width, + true + ); + backgrounds.push({ + image: 'url(\'' + preview.canvas.toDataURL() + '\')', + position: (position || 'left') + ' top', + size: preview.width + 'px ' + preview.height + 'px', + repeat: position ? 'repeat-y' : 'repeat', + origin: 'padding-box', + }); + } + + var bg = { + image: [], + position: [], + size: [], + repeat: [], + origin: [], + }; + for (var i = 0; i < backgrounds.length; i += 1) { + bg.image.push(backgrounds[i].image); + bg.position.push(backgrounds[i].position); + bg.size.push(backgrounds[i].size); + bg.repeat.push(backgrounds[i].repeat); + bg.origin.push(backgrounds[i].origin); + } + + // set previewElement's background-images + var sty = { + 'background-image': bg.image.join(', '), + 'background-position': bg.position.join(', '), + 'background-size': bg.size.join(', '), + 'background-repeat': bg.repeat.join(', '), + 'background-origin': bg.origin.join(', '), + }; + jsc.setStyle(this.previewElement, sty, this.forceStyle); + + + // set/restore previewElement's padding + var padding = { + left: null, + right: null, + }; + if (position) { + padding[position] = (this.previewSize + this.previewPadding) + 'px'; + } + + var sty = { + 'padding-left': padding.left, + 'padding-right': padding.right, + }; + jsc.setStyle(this.previewElement, sty, this.forceStyle, true); + }; + + + this.setValueElementValue = function (str) { + if (this.valueElement) { + if (jsc.nodeName(this.valueElement) === 'input') { + this.valueElement.value = str; + } else { + this.valueElement.innerHTML = str; + } + } + }; + + + this.setAlphaElementValue = function (str) { + if (this.alphaElement) { + if (jsc.nodeName(this.alphaElement) === 'input') { + this.alphaElement.value = str; + } else { + this.alphaElement.innerHTML = str; + } + } + }; + + + this._processParentElementsInDOM = function () { + if (this._linkedElementsProcessed) { return; } + this._linkedElementsProcessed = true; + + var elm = this.targetElement; + do { + // If the target element or one of its parent nodes has fixed position, + // then use fixed positioning instead + var compStyle = jsc.getCompStyle(elm); + if (compStyle.position && compStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // Ensure to attach onParentScroll only once to each parent element + // (multiple targetElements can share the same parent nodes) + // + // Note: It's not just offsetParents that can be scrollable, + // that's why we loop through all parent nodes + if (!jsc.getData(elm, 'hasScrollListener')) { + elm.addEventListener('scroll', jsc.onParentScroll, false); + jsc.setData(elm, 'hasScrollListener', true); + } + } + } while ((elm = elm.parentNode) && jsc.nodeName(elm) !== 'body'); + }; + + + this.tryHide = function () { + if (this.hideOnLeave) { + this.hide(); + } + }; + + + function setOption (option, value) { + if (typeof option !== 'string') { + throw new Error('Invalid value for option name: ' + option); + } + + // enum option + if (jsc.enumOpts.hasOwnProperty(option)) { + if (typeof value === 'string') { // enum string values are case insensitive + value = value.toLowerCase(); + } + if (jsc.enumOpts[option].indexOf(value) === -1) { + throw new Error('Option \'' + option + '\' has invalid value: ' + value); + } + } + + // deprecated option + if (jsc.deprecatedOpts.hasOwnProperty(option)) { + var oldOpt = option; + var newOpt = jsc.deprecatedOpts[option]; + if (newOpt) { + // if we have a new name for this option, let's log a warning and use the new name + console.warn('Option \'%s\' is DEPRECATED, using \'%s\' instead.' + jsc.docsRef, oldOpt, newOpt); + option = newOpt; + } else { + // new name not available for the option + throw new Error('Option \'' + option + '\' is DEPRECATED'); + } + } + + if (!(option in THIS)) { + throw new Error('Unrecognized configuration option: ' + option); + } + + THIS[option] = value; + return true; + } + + + function getOption (option) { + // deprecated option + if (jsc.deprecatedOpts.hasOwnProperty(option)) { + var oldOpt = option; + var newOpt = jsc.deprecatedOpts[option]; + if (newOpt) { + // if we have a new name for this option, let's log a warning and use the new name + console.warn('Option \'%s\' is DEPRECATED, using \'%s\' instead.' + jsc.docsRef, oldOpt, newOpt); + option = newOpt; + } else { + // new name not available for the option + throw new Error('Option \'' + option + '\' is DEPRECATED'); + } + } + + if (!(option in THIS)) { + throw new Error('Unrecognized configuration option: ' + option); + } + + return THIS[option]; + } + + + function detachPicker () { + jsc.removeClass(THIS.targetElement, jsc.pub.activeClassName); + jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap); + delete jsc.picker.owner; + } + + + function drawPicker () { + + // At this point, when drawing the picker, we know what the parent elements are + // and we can do all related DOM operations, such as registering events on them + // or checking their positioning + THIS._processParentElementsInDOM(); + + if (!jsc.picker) { + jsc.picker = { + owner: null, // owner picker instance + wrap : jsc.createEl('div'), + box : jsc.createEl('div'), + boxS : jsc.createEl('div'), // shadow area + boxB : jsc.createEl('div'), // border + pad : jsc.createEl('div'), + padB : jsc.createEl('div'), // border + padM : jsc.createEl('div'), // mouse/touch area + padPal : jsc.createPalette(), + cross : jsc.createEl('div'), + crossBY : jsc.createEl('div'), // border Y + crossBX : jsc.createEl('div'), // border X + crossLY : jsc.createEl('div'), // line Y + crossLX : jsc.createEl('div'), // line X + sld : jsc.createEl('div'), // slider + sldB : jsc.createEl('div'), // border + sldM : jsc.createEl('div'), // mouse/touch area + sldGrad : jsc.createSliderGradient(), + sldPtrS : jsc.createEl('div'), // slider pointer spacer + sldPtrIB : jsc.createEl('div'), // slider pointer inner border + sldPtrMB : jsc.createEl('div'), // slider pointer middle border + sldPtrOB : jsc.createEl('div'), // slider pointer outer border + asld : jsc.createEl('div'), // alpha slider + asldB : jsc.createEl('div'), // border + asldM : jsc.createEl('div'), // mouse/touch area + asldGrad : jsc.createASliderGradient(), + asldPtrS : jsc.createEl('div'), // slider pointer spacer + asldPtrIB : jsc.createEl('div'), // slider pointer inner border + asldPtrMB : jsc.createEl('div'), // slider pointer middle border + asldPtrOB : jsc.createEl('div'), // slider pointer outer border + btn : jsc.createEl('div'), + btnT : jsc.createEl('span'), // text + }; + + jsc.picker.pad.appendChild(jsc.picker.padPal.elm); + jsc.picker.padB.appendChild(jsc.picker.pad); + jsc.picker.cross.appendChild(jsc.picker.crossBY); + jsc.picker.cross.appendChild(jsc.picker.crossBX); + jsc.picker.cross.appendChild(jsc.picker.crossLY); + jsc.picker.cross.appendChild(jsc.picker.crossLX); + jsc.picker.padB.appendChild(jsc.picker.cross); + jsc.picker.box.appendChild(jsc.picker.padB); + jsc.picker.box.appendChild(jsc.picker.padM); + + jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm); + jsc.picker.sldB.appendChild(jsc.picker.sld); + jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB); + jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB); + jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB); + jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS); + jsc.picker.box.appendChild(jsc.picker.sldB); + jsc.picker.box.appendChild(jsc.picker.sldM); + + jsc.picker.asld.appendChild(jsc.picker.asldGrad.elm); + jsc.picker.asldB.appendChild(jsc.picker.asld); + jsc.picker.asldB.appendChild(jsc.picker.asldPtrOB); + jsc.picker.asldPtrOB.appendChild(jsc.picker.asldPtrMB); + jsc.picker.asldPtrMB.appendChild(jsc.picker.asldPtrIB); + jsc.picker.asldPtrIB.appendChild(jsc.picker.asldPtrS); + jsc.picker.box.appendChild(jsc.picker.asldB); + jsc.picker.box.appendChild(jsc.picker.asldM); + + jsc.picker.btn.appendChild(jsc.picker.btnT); + jsc.picker.box.appendChild(jsc.picker.btn); + + jsc.picker.boxB.appendChild(jsc.picker.box); + jsc.picker.wrap.appendChild(jsc.picker.boxS); + jsc.picker.wrap.appendChild(jsc.picker.boxB); + + jsc.picker.wrap.addEventListener('touchstart', jsc.onPickerTouchStart, + jsc.isPassiveEventSupported ? {passive: false} : false); + } + + var p = jsc.picker; + + var displaySlider = !!jsc.getSliderChannel(THIS); + var displayAlphaSlider = THIS.hasAlphaChannel(); + var dims = jsc.getPickerDims(THIS); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var controlPadding = jsc.getControlPadding(THIS); + var borderRadius = Math.min( + THIS.borderRadius, + Math.round(THIS.padding * Math.PI)); // px + var padCursor = 'crosshair'; + + // wrap + p.wrap.className = 'jscolor-picker-wrap'; + p.wrap.style.clear = 'both'; + p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.zIndex = THIS.zIndex; + + // picker + p.box.className = 'jscolor-picker'; + p.box.style.width = dims[0] + 'px'; + p.box.style.height = dims[1] + 'px'; + p.box.style.position = 'relative'; + + // picker shadow + p.boxS.className = 'jscolor-picker-shadow'; + p.boxS.style.position = 'absolute'; + p.boxS.style.left = '0'; + p.boxS.style.top = '0'; + p.boxS.style.width = '100%'; + p.boxS.style.height = '100%'; + jsc.setBorderRadius(p.boxS, borderRadius + 'px'); + + // picker border + p.boxB.className = 'jscolor-picker-border'; + p.boxB.style.position = 'relative'; + p.boxB.style.border = THIS.borderWidth + 'px solid'; + p.boxB.style.borderColor = THIS.borderColor; + p.boxB.style.background = THIS.backgroundColor; + jsc.setBorderRadius(p.boxB, borderRadius + 'px'); + + // IE hack: + // If the element is transparent, IE will trigger the event on the elements under it, + // e.g. on Canvas or on elements with border + p.padM.style.background = 'rgba(255,0,0,.2)'; + p.sldM.style.background = 'rgba(0,255,0,.2)'; + p.asldM.style.background = 'rgba(0,0,255,.2)'; + + p.padM.style.opacity = + p.sldM.style.opacity = + p.asldM.style.opacity = + '0'; + + // pad + p.pad.style.position = 'relative'; + p.pad.style.width = THIS.width + 'px'; + p.pad.style.height = THIS.height + 'px'; + + // pad palettes (HSV and HVS) + p.padPal.draw(THIS.width, THIS.height, jsc.getPadYChannel(THIS)); + + // pad border + p.padB.style.position = 'absolute'; + p.padB.style.left = THIS.padding + 'px'; + p.padB.style.top = THIS.padding + 'px'; + p.padB.style.border = THIS.controlBorderWidth + 'px solid'; + p.padB.style.borderColor = THIS.controlBorderColor; + + // pad mouse area + p.padM.style.position = 'absolute'; + p.padM.style.left = 0 + 'px'; + p.padM.style.top = 0 + 'px'; + p.padM.style.width = (THIS.padding + 2 * THIS.controlBorderWidth + THIS.width + controlPadding) + 'px'; + p.padM.style.height = (2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height) + 'px'; + p.padM.style.cursor = padCursor; + jsc.setData(p.padM, { + instance: THIS, + control: 'pad', + }) + + // pad cross + p.cross.style.position = 'absolute'; + p.cross.style.left = + p.cross.style.top = + '0'; + p.cross.style.width = + p.cross.style.height = + crossOuterSize + 'px'; + + // pad cross border Y and X + p.crossBY.style.position = + p.crossBX.style.position = + 'absolute'; + p.crossBY.style.background = + p.crossBX.style.background = + THIS.pointerBorderColor; + p.crossBY.style.width = + p.crossBX.style.height = + (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.crossBY.style.height = + p.crossBX.style.width = + crossOuterSize + 'px'; + p.crossBY.style.left = + p.crossBX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px'; + p.crossBY.style.top = + p.crossBX.style.left = + '0'; + + // pad cross line Y and X + p.crossLY.style.position = + p.crossLX.style.position = + 'absolute'; + p.crossLY.style.background = + p.crossLX.style.background = + THIS.pointerColor; + p.crossLY.style.height = + p.crossLX.style.width = + (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px'; + p.crossLY.style.width = + p.crossLX.style.height = + THIS.pointerThickness + 'px'; + p.crossLY.style.left = + p.crossLX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px'; + p.crossLY.style.top = + p.crossLX.style.left = + THIS.pointerBorderWidth + 'px'; + + + // slider + p.sld.style.overflow = 'hidden'; + p.sld.style.width = THIS.sliderSize + 'px'; + p.sld.style.height = THIS.height + 'px'; + + // slider gradient + p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000'); + + // slider border + p.sldB.style.display = displaySlider ? 'block' : 'none'; + p.sldB.style.position = 'absolute'; + p.sldB.style.left = (THIS.padding + THIS.width + 2 * THIS.controlBorderWidth + 2 * controlPadding) + 'px'; + p.sldB.style.top = THIS.padding + 'px'; + p.sldB.style.border = THIS.controlBorderWidth + 'px solid'; + p.sldB.style.borderColor = THIS.controlBorderColor; + + // slider mouse area + p.sldM.style.display = displaySlider ? 'block' : 'none'; + p.sldM.style.position = 'absolute'; + p.sldM.style.left = (THIS.padding + THIS.width + 2 * THIS.controlBorderWidth + controlPadding) + 'px'; + p.sldM.style.top = 0 + 'px'; + p.sldM.style.width = ( + (THIS.sliderSize + 2 * controlPadding + 2 * THIS.controlBorderWidth) + + (displayAlphaSlider ? 0 : Math.max(0, THIS.padding - controlPadding)) // remaining padding to the right edge + ) + 'px'; + p.sldM.style.height = (2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height) + 'px'; + p.sldM.style.cursor = 'default'; + jsc.setData(p.sldM, { + instance: THIS, + control: 'sld', + }) + + // slider pointer inner and outer border + p.sldPtrIB.style.border = + p.sldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // slider pointer outer border + p.sldPtrOB.style.position = 'absolute'; + p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.sldPtrOB.style.top = '0'; + + // slider pointer middle border + p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // slider pointer spacer + p.sldPtrS.style.width = THIS.sliderSize + 'px'; + p.sldPtrS.style.height = jsc.pub.sliderInnerSpace + 'px'; + + + // alpha slider + p.asld.style.overflow = 'hidden'; + p.asld.style.width = THIS.sliderSize + 'px'; + p.asld.style.height = THIS.height + 'px'; + + // alpha slider gradient + p.asldGrad.draw(THIS.sliderSize, THIS.height, '#000'); + + // alpha slider border + p.asldB.style.display = displayAlphaSlider ? 'block' : 'none'; + p.asldB.style.position = 'absolute'; + p.asldB.style.left = ( + (THIS.padding + THIS.width + 2 * THIS.controlBorderWidth + controlPadding) + + (displaySlider ? (THIS.sliderSize + 3 * controlPadding + 2 * THIS.controlBorderWidth) : 0) + ) + 'px'; + p.asldB.style.top = THIS.padding + 'px'; + p.asldB.style.border = THIS.controlBorderWidth + 'px solid'; + p.asldB.style.borderColor = THIS.controlBorderColor; + + // alpha slider mouse area + p.asldM.style.display = displayAlphaSlider ? 'block' : 'none'; + p.asldM.style.position = 'absolute'; + p.asldM.style.left = ( + (THIS.padding + THIS.width + 2 * THIS.controlBorderWidth + controlPadding) + + (displaySlider ? (THIS.sliderSize + 2 * controlPadding + 2 * THIS.controlBorderWidth) : 0) + ) + 'px'; + p.asldM.style.top = 0 + 'px'; + p.asldM.style.width = ( + (THIS.sliderSize + 2 * controlPadding + 2 * THIS.controlBorderWidth) + + Math.max(0, THIS.padding - controlPadding) // remaining padding to the right edge + ) + 'px'; + p.asldM.style.height = (2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height) + 'px'; + p.asldM.style.cursor = 'default'; + jsc.setData(p.asldM, { + instance: THIS, + control: 'asld', + }) + + // alpha slider pointer inner and outer border + p.asldPtrIB.style.border = + p.asldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // alpha slider pointer outer border + p.asldPtrOB.style.position = 'absolute'; + p.asldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.asldPtrOB.style.top = '0'; + + // alpha slider pointer middle border + p.asldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // alpha slider pointer spacer + p.asldPtrS.style.width = THIS.sliderSize + 'px'; + p.asldPtrS.style.height = jsc.pub.sliderInnerSpace + 'px'; + + + // the Close button + function setBtnBorder () { + var insetColors = THIS.controlBorderColor.split(/\s+/); + var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1]; + p.btn.style.borderColor = outsetColor; + } + var btnPadding = 15; // px + p.btn.className = 'jscolor-btn-close'; + p.btn.style.display = THIS.closeButton ? 'block' : 'none'; + p.btn.style.position = 'absolute'; + p.btn.style.left = THIS.padding + 'px'; + p.btn.style.bottom = THIS.padding + 'px'; + p.btn.style.padding = '0 ' + btnPadding + 'px'; + p.btn.style.maxWidth = (dims[0] - 2 * THIS.padding - 2 * THIS.controlBorderWidth - 2 * btnPadding) + 'px'; + p.btn.style.overflow = 'hidden'; + p.btn.style.height = THIS.buttonHeight + 'px'; + p.btn.style.whiteSpace = 'nowrap'; + p.btn.style.border = THIS.controlBorderWidth + 'px solid'; + setBtnBorder(); + p.btn.style.color = THIS.buttonColor; + p.btn.style.font = '12px sans-serif'; + p.btn.style.textAlign = 'center'; + p.btn.style.cursor = 'pointer'; + p.btn.onmousedown = function () { + THIS.hide(); + }; + p.btnT.style.lineHeight = THIS.buttonHeight + 'px'; + p.btnT.innerHTML = ''; + p.btnT.appendChild(document.createTextNode(THIS.closeText)); + + // reposition the pointers + redrawPad(); + redrawSld(); + redrawASld(); + + // If we are changing the owner without first closing the picker, + // make sure to first deal with the old owner + if (jsc.picker.owner && jsc.picker.owner !== THIS) { + jsc.removeClass(jsc.picker.owner.targetElement, jsc.pub.activeClassName); + } + + // Set a new picker owner + jsc.picker.owner = THIS; + + // The redrawPosition() method needs picker.owner to be set, that's why we call it here, + // after setting the owner + if (THIS.container === document.body) { + jsc.redrawPosition(); + } else { + jsc._drawPosition(THIS, 0, 0, 'relative', false); + } + + if (p.wrap.parentNode !== THIS.container) { + THIS.container.appendChild(p.wrap); + } + + jsc.addClass(THIS.targetElement, jsc.pub.activeClassName); + } + + + function redrawPad () { + // redraw the pad pointer + var yChannel = jsc.getPadYChannel(THIS); + var x = Math.round((THIS.channels.h / 360) * (THIS.width - 1)); + var y = Math.round((1 - THIS.channels[yChannel] / 100) * (THIS.height - 1)); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var ofs = -Math.floor(crossOuterSize / 2); + jsc.picker.cross.style.left = (x + ofs) + 'px'; + jsc.picker.cross.style.top = (y + ofs) + 'px'; + + // redraw the slider + switch (jsc.getSliderChannel(THIS)) { + case 's': + var rgb1 = jsc.HSV_RGB(THIS.channels.h, 100, THIS.channels.v); + var rgb2 = jsc.HSV_RGB(THIS.channels.h, 0, THIS.channels.v); + var color1 = 'rgb(' + + Math.round(rgb1[0]) + ',' + + Math.round(rgb1[1]) + ',' + + Math.round(rgb1[2]) + ')'; + var color2 = 'rgb(' + + Math.round(rgb2[0]) + ',' + + Math.round(rgb2[1]) + ',' + + Math.round(rgb2[2]) + ')'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + case 'v': + var rgb = jsc.HSV_RGB(THIS.channels.h, THIS.channels.s, 100); + var color1 = 'rgb(' + + Math.round(rgb[0]) + ',' + + Math.round(rgb[1]) + ',' + + Math.round(rgb[2]) + ')'; + var color2 = '#000'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + } + + // redraw the alpha slider + jsc.picker.asldGrad.draw(THIS.sliderSize, THIS.height, THIS.toHEXString()); + } + + + function redrawSld () { + var sldChannel = jsc.getSliderChannel(THIS); + if (sldChannel) { + // redraw the slider pointer + var y = Math.round((1 - THIS.channels[sldChannel] / 100) * (THIS.height - 1)); + jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(jsc.pub.sliderInnerSpace / 2)) + 'px'; + } + + // redraw the alpha slider + jsc.picker.asldGrad.draw(THIS.sliderSize, THIS.height, THIS.toHEXString()); + } + + + function redrawASld () { + var y = Math.round((1 - THIS.channels.a) * (THIS.height - 1)); + jsc.picker.asldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(jsc.pub.sliderInnerSpace / 2)) + 'px'; + } + + + function isPickerOwner () { + return jsc.picker && jsc.picker.owner === THIS; + } + + + function onValueKeyDown (ev) { + if (jsc.eventKey(ev) === 'Enter') { + if (THIS.valueElement) { + THIS.processValueInput(THIS.valueElement.value); + } + THIS.tryHide(); + } + } + + + function onAlphaKeyDown (ev) { + if (jsc.eventKey(ev) === 'Enter') { + if (THIS.alphaElement) { + THIS.processAlphaInput(THIS.alphaElement.value); + } + THIS.tryHide(); + } + } + + + function onValueChange (ev) { + if (jsc.getData(ev, 'internal')) { + return; // skip if the event was internally triggered by jscolor + } + + var oldVal = THIS.valueElement.value; + + THIS.processValueInput(THIS.valueElement.value); // this might change the value + + jsc.triggerCallback(THIS, 'onChange'); + + if (THIS.valueElement.value !== oldVal) { + // value was additionally changed -> let's trigger the change event again, even though it was natively dispatched + jsc.triggerInputEvent(THIS.valueElement, 'change', true, true); + } + } + + + function onAlphaChange (ev) { + if (jsc.getData(ev, 'internal')) { + return; // skip if the event was internally triggered by jscolor + } + + var oldVal = THIS.alphaElement.value; + + THIS.processAlphaInput(THIS.alphaElement.value); // this might change the value + + jsc.triggerCallback(THIS, 'onChange'); + + // triggering valueElement's onChange (because changing alpha changes the entire color, e.g. with rgba format) + jsc.triggerInputEvent(THIS.valueElement, 'change', true, true); + + if (THIS.alphaElement.value !== oldVal) { + // value was additionally changed -> let's trigger the change event again, even though it was natively dispatched + jsc.triggerInputEvent(THIS.alphaElement, 'change', true, true); + } + } + + + function onValueInput (ev) { + if (jsc.getData(ev, 'internal')) { + return; // skip if the event was internally triggered by jscolor + } + + if (THIS.valueElement) { + THIS.fromString(THIS.valueElement.value, jsc.flags.leaveValue); + } + + jsc.triggerCallback(THIS, 'onInput'); + + // triggering valueElement's onInput + // (not needed, it was dispatched normally by the browser) + } + + + function onAlphaInput (ev) { + if (jsc.getData(ev, 'internal')) { + return; // skip if the event was internally triggered by jscolor + } + + if (THIS.alphaElement) { + THIS.fromHSVA(null, null, null, parseFloat(THIS.alphaElement.value), jsc.flags.leaveAlpha); + } + + jsc.triggerCallback(THIS, 'onInput'); + + // triggering valueElement's onInput (because changing alpha changes the entire color, e.g. with rgba format) + jsc.triggerInputEvent(THIS.valueElement, 'input', true, true); + } + + + // + // Install the color picker on chosen element(s) + // + + + // Determine picker's container element + if (this.container === undefined) { + this.container = document.body; // default container is BODY element + + } else { // explicitly set to custom element + this.container = jsc.node(this.container); + } + + if (!this.container) { + throw new Error('Cannot instantiate color picker without a container element'); + } + + + // Fetch the target element + this.targetElement = jsc.node(targetElement); + + if (!this.targetElement) { + // temporarily customized error message to help with migrating from versions prior to 2.2 + if (typeof targetElement === 'string' && /^[a-zA-Z][\w:.-]*$/.test(targetElement)) { + // targetElement looks like valid ID + var possiblyId = targetElement; + throw new Error('If \'' + possiblyId + '\' is supposed to be an ID, please use \'#' + possiblyId + '\' or any valid CSS selector.'); + } + + throw new Error('Cannot instantiate color picker without a target element'); + } + + if (this.targetElement.jscolor && this.targetElement.jscolor instanceof jsc.pub) { + throw new Error('Color picker already installed on this element'); + } + + + // link this instance with the target element + this.targetElement.jscolor = this; + jsc.addClass(this.targetElement, jsc.pub.className); + + // register this instance + jsc.instances.push(this); + + + // if target is BUTTON + if (jsc.isButton(this.targetElement)) { + + if (this.targetElement.type.toLowerCase() !== 'button') { + // on buttons, always force type to be 'button', e.g. in situations the target