diff --git a/build/depends.py b/build/depends.py index 202419e6199..ac5e778feee 100644 --- a/build/depends.py +++ b/build/depends.py @@ -913,6 +913,8 @@ def sources(self, build): "src/controllers/softtakeover.cpp", "src/controllers/keyboard/keyboardeventfilter.cpp", "src/controllers/colorjsproxy.cpp", + "src/controllers/colormapper.cpp", + "src/controllers/colormapperjsproxy.cpp", "src/main.cpp", "src/mixxx.cpp", diff --git a/res/controllers/Roland_DJ-505-scripts.js b/res/controllers/Roland_DJ-505-scripts.js index 2e2777c0e89..25efbbfb523 100644 --- a/res/controllers/Roland_DJ-505-scripts.js +++ b/res/controllers/Roland_DJ-505-scripts.js @@ -941,17 +941,24 @@ DJ505.PadColor = { DIM_MODIFIER: 0x10, }; -DJ505.PadColorMap = [ - DJ505.PadColor.OFF, - DJ505.PadColor.RED, - DJ505.PadColor.GREEN, - DJ505.PadColor.BLUE, - DJ505.PadColor.YELLOW, - DJ505.PadColor.CELESTE, - DJ505.PadColor.PURPLE, - DJ505.PadColor.APRICOT, - DJ505.PadColor.WHITE, -]; +DJ505.PadColorMap = new ColorMapper({ + '#FFCC0000': DJ505.PadColor.RED, + '#FFCC4400': DJ505.PadColor.CORAL, + '#FFCC8800': DJ505.PadColor.ORANGE, + '#FFCCCC00': DJ505.PadColor.YELLOW, + '#FF88CC00': DJ505.PadColor.GREEN, + '#FF00CC00': DJ505.PadColor.APPLEGREEN, + '#FF00CC88': DJ505.PadColor.AQUAMARINE, + '#FF00CCCC': DJ505.PadColor.TURQUOISE, + '#FF0088CC': DJ505.PadColor.CELESTE, + '#FF0000CC': DJ505.PadColor.BLUE, + '#FF4400CC': DJ505.PadColor.AZURE, + '#FF8800CC': DJ505.PadColor.PURPLE, + '#FFCC00CC': DJ505.PadColor.MAGENTA, + '#FFCC0044': DJ505.PadColor.RED, + '#FFFFCCCC': DJ505.PadColor.APRICOT, + '#FFFFFFFF': DJ505.PadColor.WHITE, +}); DJ505.PadSection = function (deck, offset) { // TODO: Add support for missing modes (flip, slicer, slicerloop) @@ -1190,7 +1197,6 @@ DJ505.HotcueMode = function (deck, offset) { this.ledControl = DJ505.PadMode.HOTCUE; this.color = DJ505.PadColor.WHITE; - var hotcueColors = [this.color].concat(DJ505.PadColorMap.slice(1)); this.pads = new components.ComponentContainer(); for (var i = 0; i <= 7; i++) { this.pads[i] = new components.HotcueButton({ @@ -1202,7 +1208,7 @@ DJ505.HotcueMode = function (deck, offset) { group: deck.currentDeck, on: this.color, off: this.color + DJ505.PadColor.DIM_MODIFIER, - colors: hotcueColors, + colorMapper: DJ505.PadColorMap, outConnect: false, }); } @@ -1226,7 +1232,6 @@ DJ505.CueLoopMode = function (deck, offset) { this.ledControl = DJ505.PadMode.HOTCUE; this.color = DJ505.PadColor.BLUE; - var cueloopColors = [this.color].concat(DJ505.PadColorMap.slice(1)); this.PerformancePad = function(n) { this.midi = [0x94 + offset, 0x14 + n]; this.number = n + 1; @@ -1241,7 +1246,7 @@ DJ505.CueLoopMode = function (deck, offset) { group: deck.currentDeck, on: this.color, off: this.color + DJ505.PadColor.DIM_MODIFIER, - colors: cueloopColors, + colorMapper: DJ505.PadColorMap, outConnect: false, unshift: function() { this.input = function (channel, control, value, status, group) { @@ -1492,13 +1497,12 @@ DJ505.PitchPlayMode = function (deck, offset) { this.color = DJ505.PadColor.GREEN; this.cuepoint = 1; this.range = PitchPlayRange.MID; - var pitchplayColors = [this.color].concat(DJ505.PadColorMap.slice(1)); this.PerformancePad = function(n) { this.midi = [0x94 + offset, 0x14 + n]; this.number = n + 1; this.on = this.color + DJ505.PadColor.DIM_MODIFIER; - this.colors = pitchplayColors; + this.colorMapper = DJ505.PadColorMap; this.colorKey = 'hotcue_' + this.number + '_color'; components.Button.call(this); }; @@ -1510,10 +1514,10 @@ DJ505.PitchPlayMode = function (deck, offset) { mode: this, outConnect: false, off: DJ505.PadColor.OFF, - outputColor: function(id) { + outputColor: function(colorCode) { // For colored hotcues (shifted only) - var color = this.colors[id]; - this.send((this.mode.cuepoint === this.number) ? color : (color + DJ505.PadColor.DIM_MODIFIER)); + var midiColor = this.colorMapper.getNearestValue(colorCode); + this.send((this.mode.cuepoint === this.number) ? midiColor : (midiColor + DJ505.PadColor.DIM_MODIFIER)); }, unshift: function() { this.outKey = "pitch_adjust"; diff --git a/res/controllers/midi-components-0.0.js b/res/controllers/midi-components-0.0.js index 1893768c2a6..9c07c14b723 100644 --- a/res/controllers/midi-components-0.0.js +++ b/res/controllers/midi-components-0.0.js @@ -294,11 +294,8 @@ print('ERROR: No hotcue number specified for new HotcueButton.'); return; } - if (options.colors !== undefined || options.sendRGB !== undefined) { + if (options.colorMapper !== undefined || options.sendRGB !== undefined) { this.colorKey = 'hotcue_' + options.number + '_color'; - if (options.colors === undefined) { - options.colors = color.hotcueColorPalette(); - } } this.number = options.number; this.outKey = 'hotcue_' + this.number + '_enabled'; @@ -330,20 +327,16 @@ this.send(outval); } }, - outputColor: function (id) { - var color = this.colors[id]; - if (color instanceof Array) { - if (color.length !== 3) { - print("ERROR: invalid color array for id: " + id); - return; - } + outputColor: function (colorCode) { + if (this.colorMapper !== undefined) { + var nearestColorValue = this.colorMapper.getNearestValue(colorCode); + this.send(nearestColorValue); + } else { if (this.sendRGB === undefined) { print("ERROR: no function defined for sending RGB colors"); return; } - this.sendRGB(color); - } else if (typeof color === 'number') { - this.send(color); + this.sendRGB(color.colorFromHexCode(colorCode)); } }, connect: function() { diff --git a/src/controllers/colormapper.cpp b/src/controllers/colormapper.cpp new file mode 100644 index 00000000000..3caa515806e --- /dev/null +++ b/src/controllers/colormapper.cpp @@ -0,0 +1,59 @@ +#include "controllers/colormapper.h" + +#include +#include + +#include "util/debug.h" + +namespace { +double colorDistance(QRgb a, QRgb b) { + // This algorithm calculates the distance between two colors. In + // contrast to the L2 norm, this also tries take the human perception + // of colors into account. More accurate algorithms like the CIELAB2000 + // Delta-E rely on sophisticated color space conversions and need a lot + // of costly computations. In contrast, this is a low-cost + // approximation and should be sufficently accurate. + // More details: https://www.compuphase.com/cmetric.htm + long mean_red = ((long)qRed(a) + (long)qRed(b)) / 2; + long delta_red = (long)qRed(a) - (long)qRed(b); + long delta_green = (long)qGreen(a) - (long)qGreen(b); + long delta_blue = (long)qBlue(a) - (long)qBlue(b); + return sqrt( + (((512 + mean_red) * delta_red * delta_red) >> 8) + + (4 * delta_green * delta_green) + + (((767 - mean_red) * delta_blue * delta_blue) >> 8)); +} +} // namespace + +QPair ColorMapper::getNearestColor(QRgb desiredColor) { + // If desired color is already in cache, use cache entry + QMap::const_iterator i = m_cache.find(desiredColor); + QMap::const_iterator j; + if (i != m_cache.constEnd()) { + j = m_availableColors.find(i.value()); + DEBUG_ASSERT(j != m_availableColors.constEnd()); + qDebug() << "ColorMapper cache hit for" << desiredColor << ":" + << "Color =" << j.key() << "," + << "Value =" << j.value(); + return QPair(j.key(), j.value()); + } + + // Color is not cached + QMap::const_iterator nearestColorIterator; + double nearestColorDistance = qInf(); + for (j = m_availableColors.constBegin(); j != m_availableColors.constEnd(); j++) { + QRgb availableColor = j.key(); + double distance = colorDistance(desiredColor, availableColor); + if (distance < nearestColorDistance) { + nearestColorDistance = distance; + nearestColorIterator = j; + } + } + + DEBUG_ASSERT(nearestColorDistance < qInf()); + qDebug() << "ColorMapper found matching color for" << desiredColor << ":" + << "Color =" << nearestColorIterator.key() << "," + << "Value =" << nearestColorIterator.value(); + m_cache.insert(desiredColor, nearestColorIterator.key()); + return QPair(nearestColorIterator.key(), nearestColorIterator.value()); +} diff --git a/src/controllers/colormapper.h b/src/controllers/colormapper.h new file mode 100644 index 00000000000..c138edbf38d --- /dev/null +++ b/src/controllers/colormapper.h @@ -0,0 +1,30 @@ +#ifndef COLORMAPPER_H +#define COLORMAPPER_H + +#include +#include +#include +#include +#include + +#include "util/assert.h" + +class ColorMapper final : public QObject { + Q_OBJECT + public: + ColorMapper() = delete; + ColorMapper(const QMap availableColors) + : m_availableColors(availableColors) { + DEBUG_ASSERT(!m_availableColors.isEmpty()); + } + + ~ColorMapper() = default; + + QPair getNearestColor(QRgb desiredColor); + + private: + const QMap m_availableColors; + QMap m_cache; +}; + +#endif /* COLORMAPPER_H */ diff --git a/src/controllers/colormapperjsproxy.cpp b/src/controllers/colormapperjsproxy.cpp new file mode 100644 index 00000000000..f806cb9752e --- /dev/null +++ b/src/controllers/colormapperjsproxy.cpp @@ -0,0 +1,47 @@ +#include + +#include "controllers/colormapperjsproxy.h" + +ColorMapperJSProxy::ColorMapperJSProxy(QScriptEngine* pScriptEngine, QMap availableColors) + : m_pScriptEngine(pScriptEngine) { + m_colorMapper = new ColorMapper(availableColors); +} + +QScriptValue ColorMapperJSProxy::getNearestColor(uint colorCode) { + QPair result = m_colorMapper->getNearestColor(static_cast(colorCode)); + QScriptValue jsColor = m_pScriptEngine->newObject(); + jsColor.setProperty("red", qRed(result.first)); + jsColor.setProperty("green", qGreen(result.first)); + jsColor.setProperty("blue", qBlue(result.first)); + jsColor.setProperty("alpha", qAlpha(result.first)); + return jsColor; +} + +QScriptValue ColorMapperJSProxy::getNearestValue(uint colorCode) { + QPair result = m_colorMapper->getNearestColor(static_cast(colorCode)); + return m_pScriptEngine->toScriptValue(result.second); +} + +QScriptValue ColorMapperJSProxyConstructor(QScriptContext* pScriptContext, QScriptEngine* pScriptEngine) { + QMap availableColors; + DEBUG_ASSERT(pScriptContext->argumentCount() == 1); + QScriptValueIterator it(pScriptContext->argument(0)); + while (it.hasNext()) { + it.next(); + DEBUG_ASSERT(!it.value().isObject()); + QColor color(it.name()); + VERIFY_OR_DEBUG_ASSERT(color.isValid()) { + qWarning() << "Received invalid color name from controller script:" << it.name(); + continue; + } + availableColors.insert(color.rgb(), it.value().toVariant()); + } + + if (availableColors.isEmpty()) { + qWarning() << "Failed to create ColorMapper object: available colors mustn't be empty!"; + return pScriptEngine->undefinedValue(); + } + + QObject* colorMapper = new ColorMapperJSProxy(pScriptEngine, availableColors); + return pScriptEngine->newQObject(colorMapper, QScriptEngine::ScriptOwnership); +} diff --git a/src/controllers/colormapperjsproxy.h b/src/controllers/colormapperjsproxy.h new file mode 100644 index 00000000000..367d13bba3e --- /dev/null +++ b/src/controllers/colormapperjsproxy.h @@ -0,0 +1,30 @@ +#ifndef COLORMAPPERJS_H +#define COLORMAPPERJS_H + +#include +#include + +#include "controllers/colormapper.h" + +class ColorMapperJSProxy final : public QObject { + Q_OBJECT + public: + ColorMapperJSProxy() = delete; + ColorMapperJSProxy(QScriptEngine* pScriptEngine, QMap availableColors); + + ~ColorMapperJSProxy() { + delete m_colorMapper; + }; + + public slots: + QScriptValue getNearestColor(uint ColorCode); + QScriptValue getNearestValue(uint ColorCode); + + private: + QScriptEngine* m_pScriptEngine; + ColorMapper* m_colorMapper; +}; + +QScriptValue ColorMapperJSProxyConstructor(QScriptContext* context, QScriptEngine* engine); + +#endif /* COLORMAPPER_H */ diff --git a/src/controllers/controllerengine.cpp b/src/controllers/controllerengine.cpp index 9bee7c4209e..e01e8943a1c 100644 --- a/src/controllers/controllerengine.cpp +++ b/src/controllers/controllerengine.cpp @@ -6,8 +6,8 @@ email : spappalardo@mixxx.org ***************************************************************************/ +#include "controllers/colormapperjsproxy.h" #include "controllers/controllerengine.h" - #include "controllers/controller.h" #include "controllers/controllerdebug.h" #include "control/controlobject.h" @@ -219,6 +219,10 @@ void ControllerEngine::initializeScriptEngine() { m_pEngine, HotcueColorPaletteSettings(m_pConfig)); engineGlobalObject.setProperty("color", m_pEngine->newQObject(m_pColorJSProxy.get())); + QScriptValue constructor = m_pEngine->newFunction(ColorMapperJSProxyConstructor); + QScriptValue metaObject = m_pEngine->newQMetaObject(&ColorMapperJSProxy::staticMetaObject, constructor); + engineGlobalObject.setProperty("ColorMapper", metaObject); + m_pBaClass = new ByteArrayClass(m_pEngine); engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor()); }