From b36f77df7e24bd108caff1dc71c514f412851493 Mon Sep 17 00:00:00 2001 From: Andre Bossard Date: Mon, 10 Aug 2026 16:49:19 +0200 Subject: [PATCH 1/2] Refactor RGB scripts to use RGB values instead of HSV - Updated multiple RGB scripts to replace HSV color handling with direct RGB values. - Modified functions to create and manipulate pixel maps using RGB arrays. - Removed unnecessary HSV color conversions and streamlined color handling. - Adjusted color initialization and merging logic to accommodate RGB format. - Cleaned up code for better readability and maintainability. - Removed RGBW color mode references from the RGBMatrixEditor UI and logic. Signed-off-by: Andre Bossard --- engine/src/rgbalgorithm.cpp | 13 +- engine/src/rgbalgorithm.h | 12 - engine/src/rgbaudio.cpp | 265 ++++ engine/src/rgbaudio.h | 116 ++ engine/src/rgbmatrix.cpp | 1306 ++++------------- engine/src/rgbmatrix.h | 270 +--- engine/src/rgbscriptv4.cpp | 478 +----- engine/src/rgbscriptv4.h | 58 - engine/test/rgbmatrix/rgbmatrix_test.cpp | 13 +- engine/test/rgbscript/CMakeLists.txt | 14 - engine/test/rgbscript/rgbscript_test.cpp | 150 +- .../qml/fixturesfunctions/RGBMatrixEditor.qml | 464 +++--- qmlui/rgbmatrixeditor.cpp | 162 +- qmlui/rgbmatrixeditor.h | 59 - .../{rgbscripts => huescripts}/audioaurora.js | 0 .../audiobarcode.js | 0 .../audiobasslaser.js | 0 .../audiobeatcolors.js | 0 .../{rgbscripts => huescripts}/audioblocks.js | 0 .../{rgbscripts => huescripts}/audioblurz.js | 0 .../audiobuildup.js | 0 .../audiocellular.js | 0 .../{rgbscripts => huescripts}/audiochaser.js | 0 .../audiocrawler.js | 0 .../audiodjlight.js | 0 .../{rgbscripts => huescripts}/audioenergy.js | 0 .../audioenergy2.js | 0 .../audioequalizer.js | 0 .../{rgbscripts => huescripts}/audiofire.js | 0 .../audiofireworks.js | 0 .../audioflowfield.js | 0 .../{rgbscripts => huescripts}/audioglitch.js | 0 .../audioglitch2.js | 0 .../audiogravimeter.js | 0 .../audiohueshift.js | 0 .../{rgbscripts => huescripts}/audiolava.js | 0 .../{rgbscripts => huescripts}/audiomelt.js | 0 .../audiomeltsparkle.js | 0 .../{rgbscripts => huescripts}/audioplasma.js | 0 .../{rgbscripts => huescripts}/audiopower.js | 0 .../audiopuddles.js | 0 .../audioreaction.js | 0 .../audioreactor.js | 0 .../{rgbscripts => huescripts}/audioscan.js | 0 .../audioscanflare.js | 0 .../audioscanmulti.js | 0 .../audioshockwave.js | 0 .../{rgbscripts => huescripts}/audioshot.js | 0 .../{rgbscripts => huescripts}/audiosoap.js | 0 .../audiospectrum.js | 0 .../audiosplittower.js | 0 .../{rgbscripts => huescripts}/audiostrobe.js | 0 .../{rgbscripts => huescripts}/audiotunnel.js | 0 .../{rgbscripts => huescripts}/audiovortex.js | 0 .../{rgbscripts => huescripts}/audiowater.js | 0 .../{rgbscripts => huescripts}/hsvutil.js | 0 resources/rgbscripts/CMakeLists.txt | 54 - resources/rgbscripts/alternate.js | 50 +- resources/rgbscripts/balls.js | 157 +- resources/rgbscripts/blinder.js | 98 +- resources/rgbscripts/circles.js | 61 +- resources/rgbscripts/circular.js | 157 +- resources/rgbscripts/empty.js | 53 +- resources/rgbscripts/evenodd.js | 9 +- resources/rgbscripts/fill.js | 9 +- resources/rgbscripts/fillfromcenter.js | 9 +- resources/rgbscripts/fillunfill.js | 46 +- resources/rgbscripts/fillunfillfromcenter.js | 15 +- .../rgbscripts/fillunfillsquaresfromcenter.js | 17 +- resources/rgbscripts/fireworks.js | 113 +- resources/rgbscripts/flyingobjects.js | 376 +++-- resources/rgbscripts/gradient.js | 66 +- resources/rgbscripts/lines.js | 39 +- resources/rgbscripts/marquee.js | 74 +- resources/rgbscripts/noise.js | 40 +- resources/rgbscripts/onebyone.js | 14 +- resources/rgbscripts/opposite.js | 21 +- resources/rgbscripts/plasma.js | 114 +- resources/rgbscripts/randomcolumn.js | 19 +- resources/rgbscripts/randomfillcolumn.js | 13 +- resources/rgbscripts/randomfillrow.js | 13 +- resources/rgbscripts/randomfillsingle.js | 28 +- resources/rgbscripts/randompixelperrow.js | 12 +- .../rgbscripts/randompixelperrowmulticolor.js | 22 +- resources/rgbscripts/randomrow.js | 19 +- resources/rgbscripts/randomsingle.js | 26 +- resources/rgbscripts/sinewave.js | 16 +- resources/rgbscripts/snowbubbles.js | 77 +- resources/rgbscripts/squares.js | 74 +- resources/rgbscripts/squaresfromcenter.js | 13 +- resources/rgbscripts/starfield.js | 75 +- resources/rgbscripts/stripes.js | 8 +- resources/rgbscripts/stripesfromcenter.js | 8 +- resources/rgbscripts/strobe.js | 14 +- resources/rgbscripts/verticalfall.js | 46 +- resources/rgbscripts/waves.js | 26 +- ui/src/rgbmatrixeditor.cpp | 48 +- ui/src/rgbmatrixeditor.ui | 24 +- 98 files changed, 2293 insertions(+), 3260 deletions(-) create mode 100644 engine/src/rgbaudio.cpp create mode 100644 engine/src/rgbaudio.h rename resources/{rgbscripts => huescripts}/audioaurora.js (100%) rename resources/{rgbscripts => huescripts}/audiobarcode.js (100%) rename resources/{rgbscripts => huescripts}/audiobasslaser.js (100%) rename resources/{rgbscripts => huescripts}/audiobeatcolors.js (100%) rename resources/{rgbscripts => huescripts}/audioblocks.js (100%) rename resources/{rgbscripts => huescripts}/audioblurz.js (100%) rename resources/{rgbscripts => huescripts}/audiobuildup.js (100%) rename resources/{rgbscripts => huescripts}/audiocellular.js (100%) rename resources/{rgbscripts => huescripts}/audiochaser.js (100%) rename resources/{rgbscripts => huescripts}/audiocrawler.js (100%) rename resources/{rgbscripts => huescripts}/audiodjlight.js (100%) rename resources/{rgbscripts => huescripts}/audioenergy.js (100%) rename resources/{rgbscripts => huescripts}/audioenergy2.js (100%) rename resources/{rgbscripts => huescripts}/audioequalizer.js (100%) rename resources/{rgbscripts => huescripts}/audiofire.js (100%) rename resources/{rgbscripts => huescripts}/audiofireworks.js (100%) rename resources/{rgbscripts => huescripts}/audioflowfield.js (100%) rename resources/{rgbscripts => huescripts}/audioglitch.js (100%) rename resources/{rgbscripts => huescripts}/audioglitch2.js (100%) rename resources/{rgbscripts => huescripts}/audiogravimeter.js (100%) rename resources/{rgbscripts => huescripts}/audiohueshift.js (100%) rename resources/{rgbscripts => huescripts}/audiolava.js (100%) rename resources/{rgbscripts => huescripts}/audiomelt.js (100%) rename resources/{rgbscripts => huescripts}/audiomeltsparkle.js (100%) rename resources/{rgbscripts => huescripts}/audioplasma.js (100%) rename resources/{rgbscripts => huescripts}/audiopower.js (100%) rename resources/{rgbscripts => huescripts}/audiopuddles.js (100%) rename resources/{rgbscripts => huescripts}/audioreaction.js (100%) rename resources/{rgbscripts => huescripts}/audioreactor.js (100%) rename resources/{rgbscripts => huescripts}/audioscan.js (100%) rename resources/{rgbscripts => huescripts}/audioscanflare.js (100%) rename resources/{rgbscripts => huescripts}/audioscanmulti.js (100%) rename resources/{rgbscripts => huescripts}/audioshockwave.js (100%) rename resources/{rgbscripts => huescripts}/audioshot.js (100%) rename resources/{rgbscripts => huescripts}/audiosoap.js (100%) rename resources/{rgbscripts => huescripts}/audiospectrum.js (100%) rename resources/{rgbscripts => huescripts}/audiosplittower.js (100%) rename resources/{rgbscripts => huescripts}/audiostrobe.js (100%) rename resources/{rgbscripts => huescripts}/audiotunnel.js (100%) rename resources/{rgbscripts => huescripts}/audiovortex.js (100%) rename resources/{rgbscripts => huescripts}/audiowater.js (100%) rename resources/{rgbscripts => huescripts}/hsvutil.js (100%) diff --git a/engine/src/rgbalgorithm.cpp b/engine/src/rgbalgorithm.cpp index 8085892b45..62ca08ea49 100644 --- a/engine/src/rgbalgorithm.cpp +++ b/engine/src/rgbalgorithm.cpp @@ -25,6 +25,7 @@ #include "rgbscriptscache.h" #include "rgbalgorithm.h" +#include "rgbaudio.h" #include "rgbimage.h" #include "rgbplain.h" #include "rgbtext.h" @@ -71,9 +72,11 @@ QStringList RGBAlgorithm::algorithms(Doc * doc) RGBPlain plain(doc); RGBText text(doc); RGBImage image(doc); + RGBAudio audio(doc); list << plain.name(); list << text.name(); list << image.name(); + list << audio.name(); list << doc->rgbScriptsCache()->names(); return list; } @@ -82,11 +85,14 @@ RGBAlgorithm* RGBAlgorithm::algorithm(Doc * doc, const QString& name) { RGBText text(doc); RGBImage image(doc); + RGBAudio audio(doc); RGBPlain plain(doc); if (name == text.name()) return text.clone(); else if (name == image.name()) return image.clone(); + else if (name == audio.name()) + return audio.clone(); else if (name == plain.name()) return plain.clone(); else @@ -120,6 +126,12 @@ RGBAlgorithm* RGBAlgorithm::loader(Doc * doc, QXmlStreamReader &root) if (text.loadXML(root) == true) algo = text.clone(); } + else if (type == KXMLQLCRGBAudio) + { + RGBAudio audio(doc); + if (audio.loadXML(root) == true) + algo = audio.clone(); + } else if (type == KXMLQLCRGBScript) { RGBScript* scr = doc->rgbScriptsCache()->script(root.readElementText()); @@ -137,7 +149,6 @@ RGBAlgorithm* RGBAlgorithm::loader(Doc * doc, QXmlStreamReader &root) else { qWarning() << "Unrecognized RGB algorithm type:" << type; - root.skipCurrentElement(); } return algo; diff --git a/engine/src/rgbalgorithm.h b/engine/src/rgbalgorithm.h index f8db54abdc..800ef4ad58 100644 --- a/engine/src/rgbalgorithm.h +++ b/engine/src/rgbalgorithm.h @@ -22,7 +22,6 @@ #define RGBALGORITHM_H #include -#include #include #include #include @@ -106,17 +105,6 @@ class RGBAlgorithm */ virtual int acceptColors() const = 0; - /** Return whether this algorithm uses audio input data. - * When true, the host should provide audio spectrum data before rendering. */ - virtual bool usesAudio() const { return type() == Audio; } - - /** Return the top-level audio input categories this algorithm reads - * (e.g. "power", "spectrum", "beat"). Empty if not audio-aware. */ - virtual QStringList audioInputCategories() const { return {}; } - - /** Set the physical display size for algorithms rendered through transformed geometry. */ - virtual void setDisplaySize(const QSize &size) { Q_UNUSED(size); } - /************************************************************************ * RGB Colors ************************************************************************/ diff --git a/engine/src/rgbaudio.cpp b/engine/src/rgbaudio.cpp new file mode 100644 index 0000000000..3834652b84 --- /dev/null +++ b/engine/src/rgbaudio.cpp @@ -0,0 +1,265 @@ +/* + Q Light Controller Plus + rgbaudio.cpp + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include + +#include "rgbaudio.h" +#include "audiocapture.h" +#include "doc.h" + +RGBAudio::RGBAudio(Doc * doc) + : RGBAlgorithm(doc) + , m_audioInput(NULL) + , m_bandsNumber(-1) + , m_maxMagnitude(0) +{ +} + +RGBAudio::RGBAudio(const RGBAudio& a, QObject *parent) + : QObject(parent) + , RGBAlgorithm(a.doc()) + , m_audioInput(NULL) + , m_bandsNumber(-1) + , m_maxMagnitude(0) +{ +} + +RGBAudio::~RGBAudio() +{ + QSharedPointer capture(doc()->audioInputCapture()); + if (capture.data() == m_audioInput && m_bandsNumber > 0) + { + m_audioInput->unregisterBandsNumber(m_bandsNumber); + } +} + +RGBAlgorithm* RGBAudio::clone() const +{ + RGBAudio* audio = new RGBAudio(*this); + return static_cast (audio); +} + +void RGBAudio::setAudioCapture(AudioCapture* cap) +{ + qDebug() << Q_FUNC_INFO << "Audio capture set"; + + m_audioInput = cap; + connect(m_audioInput, SIGNAL(dataProcessed(double*,int,double,quint32)), + this, SLOT(slotAudioBarsChanged(double*,int,double,quint32))); + m_bandsNumber = -1; +} + +void RGBAudio::slotAudioBarsChanged(double *spectrumBands, int size, + double maxMagnitude, quint32 power) +{ + if (size != m_bandsNumber) + return; + + QMutexLocker locker(&m_mutex); + + m_spectrumValues.clear(); + for (int i = 0; i < m_bandsNumber; i++) + m_spectrumValues.append(spectrumBands[i]); + m_maxMagnitude = maxMagnitude; + m_volumePower = power; +} + +void RGBAudio::calculateColors(int barsHeight) +{ + if (barsHeight > 0) + { + QColor startColor = getColor(0); + QColor endColor = getColor(1); + m_barColors.clear(); + if (endColor == QColor() + || barsHeight == 1) // to avoid division by 0 below + { + for (int i = 0; i < barsHeight; i++) + m_barColors.append(startColor.rgb()); + } + else + { + int crDelta = (endColor.red() - startColor.red()) / (barsHeight - 1); + int cgDelta = (endColor.green() - startColor.green()) / (barsHeight - 1); + int cbDelta = (endColor.blue() - startColor.blue()) / (barsHeight - 1); + QColor pixelColor = startColor; + + for (int i = 0; i < barsHeight; i++) + { + m_barColors.append(pixelColor.rgb()); + pixelColor = QColor(pixelColor.red() + crDelta, + pixelColor.green() + cgDelta, + pixelColor.blue() + cbDelta); + } + } + } +} + +/**************************************************************************** + * RGBAlgorithm + ****************************************************************************/ + +int RGBAudio::rgbMapStepCount(const QSize& size) +{ + Q_UNUSED(size); + return 1; +} + +void RGBAudio::rgbMapSetColors(const QVector &colors) +{ + Q_UNUSED(colors); +} + +QVector RGBAudio::rgbMapGetColors() +{ + return QVector(); +} + +void RGBAudio::rgbMap(const QSize& size, uint rgb, int step, RGBMap &map) +{ + Q_UNUSED(step); + + QMutexLocker locker(&m_mutex); + + QSharedPointer capture = doc()->audioInputCapture(); + if (capture.data() != m_audioInput) + setAudioCapture(capture.data()); + + map.resize(size.height()); + for (int y = 0; y < size.height(); y++) + { + map[y].resize(size.width()); + map[y].fill(0); + } + + // on the first round, just set the proper number of + // spectrum bands to receive + if (m_bandsNumber == -1) + { + m_bandsNumber = size.width(); + qDebug() << "[RGBAudio] set" << m_bandsNumber << "bars"; + m_audioInput->registerBandsNumber(m_bandsNumber); + return; + } + if (m_barColors.count() == 0) + calculateColors(size.height()); + + double volHeight = (m_volumePower * size.height()) / 0x7FFF; + for (int x = 0; x < m_spectrumValues.count(); x++) + { + int barHeight; + if (m_maxMagnitude == 0) + barHeight = 0; + else + { + barHeight = (volHeight * m_spectrumValues[x]) / m_maxMagnitude; + if (barHeight > size.height()) + barHeight = size.height(); + } + for (int y = size.height() - barHeight; y < size.height(); y++) + { + if (m_barColors.count() == 0) + map[y][x] = rgb; + else + map[y][x] = m_barColors.at(y); + } + } +} + +void RGBAudio::postRun() +{ + QMutexLocker locker(&m_mutex); + + QSharedPointer capture = doc()->audioInputCapture(); + if (capture.data() == m_audioInput) + { + disconnect(m_audioInput, SIGNAL(dataProcessed(double*,int,double,quint32)), + this, SLOT(slotAudioBarsChanged(double*,int,double,quint32))); + if (m_bandsNumber > 0) + m_audioInput->unregisterBandsNumber(m_bandsNumber); + } + m_audioInput = NULL; + m_bandsNumber = -1; +} + +QString RGBAudio::name() const +{ + return QString("Audio Spectrum"); +} + +QString RGBAudio::author() const +{ + return QString("Massimo Callegari"); +} + +int RGBAudio::apiVersion() const +{ + return 1; +} + +void RGBAudio::setColors(QVector colors) +{ + RGBAlgorithm::setColors(colors); + + // invalidate bars colors so the next time a rendering is + // required, it will be filled with the right values + m_barColors.clear(); +} + +RGBAlgorithm::Type RGBAudio::type() const +{ + return RGBAlgorithm::Audio; +} + +int RGBAudio::acceptColors() const +{ + return 2; // start and end colors accepted +} + +bool RGBAudio::loadXML(QXmlStreamReader &root) +{ + if (root.name() != KXMLQLCRGBAlgorithm) + { + qWarning() << Q_FUNC_INFO << "RGB Algorithm node not found"; + return false; + } + + if (root.attributes().value(KXMLQLCRGBAlgorithmType).toString() != KXMLQLCRGBAudio) + { + qWarning() << Q_FUNC_INFO << "RGB Algorithm is not Audio"; + return false; + } + + root.skipCurrentElement(); + + return true; +} + +bool RGBAudio::saveXML(QXmlStreamWriter *doc) const +{ + Q_ASSERT(doc != NULL); + + doc->writeStartElement(KXMLQLCRGBAlgorithm); + doc->writeAttribute(KXMLQLCRGBAlgorithmType, KXMLQLCRGBAudio); + doc->writeEndElement(); + + return true; +} diff --git a/engine/src/rgbaudio.h b/engine/src/rgbaudio.h new file mode 100644 index 0000000000..7de3e4758c --- /dev/null +++ b/engine/src/rgbaudio.h @@ -0,0 +1,116 @@ +/* + Q Light Controller Plus + rgbaudio.h + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef RGBAUDIO_H +#define RGBAUDIO_H + +#include +#include + +#include "rgbalgorithm.h" + +/** @addtogroup engine_functions Functions + * @{ + */ + +#define KXMLQLCRGBAudio QStringLiteral("Audio") + +class AudioCapture; + +class RGBAudio final : public QObject, public RGBAlgorithm +{ + Q_OBJECT + +public: + RGBAudio(Doc * doc); + RGBAudio(const RGBAudio& t, QObject *parent = 0); + ~RGBAudio(); + + /** @reimp */ + RGBAlgorithm* clone() const override; + +private: + void setAudioCapture(AudioCapture* cap); + +protected slots: + void slotAudioBarsChanged(double *spectrumBands, int size, double maxMagnitude, quint32 power); + +private: + void calculateColors(int barsHeight = 0); + +protected: + AudioCapture *m_audioInput; + int m_bandsNumber; + QMutex m_mutex; + QVectorm_spectrumValues; + double m_maxMagnitude; + quint32 m_volumePower; + QList m_barColors; + + /************************************************************************ + * RGBAlgorithm + ************************************************************************/ +public: + /** @reimp */ + int rgbMapStepCount(const QSize& size) override; + + /** @reimp */ + void rgbMapSetColors(const QVector &colors) override; + + /** @reimp */ + QVector rgbMapGetColors() override; + + /** @reimp */ + void rgbMap(const QSize& size, uint rgb, int step, RGBMap &map) override; + + /** @reimp */ + virtual void postRun() override; + + /** @reimp */ + QString name() const override; + + /** @reimp */ + QString author() const override; + + /** @reimp */ + int apiVersion() const override; + + /** @reimp */ + void setColors(QVector colors) override; + + /** @reimp */ + RGBAlgorithm::Type type() const override; + + /** @reimp */ + int acceptColors() const override; + + /************************************************************************ + * Load & Save + ************************************************************************/ +public: + /** @reimp */ + bool loadXML(QXmlStreamReader &root) override; + + /** @reimp */ + bool saveXML(QXmlStreamWriter *doc) const override; +}; + +/** @} */ + +#endif diff --git a/engine/src/rgbmatrix.cpp b/engine/src/rgbmatrix.cpp index c99de08822..f036c089e4 100644 --- a/engine/src/rgbmatrix.cpp +++ b/engine/src/rgbmatrix.cpp @@ -23,15 +23,11 @@ #include #include #include -#include -#include #include #include #include "rgbscriptscache.h" #include "qlcfixturehead.h" -#include "audiochannel.h" -#include "audioprofile.h" #include "fixturegroup.h" #include "genericfader.h" #include "fadechannel.h" @@ -58,17 +54,6 @@ #define KXMLQLCRGBMatrixControlModeUV QStringLiteral("UV") #define KXMLQLCRGBMatrixControlModeDimmer QStringLiteral("Dimmer") #define KXMLQLCRGBMatrixControlModeShutter QStringLiteral("Shutter") -#define KXMLQLCRGBMatrixControlModeRgbw QStringLiteral("RGBW") -#define KXMLQLCRGBMatrixControlModeRgbwBrighter QStringLiteral("RGBWBrighter") - -#define KXMLQLCRGBMatrixRotation QStringLiteral("Rotation") -#define KXMLQLCRGBMatrixMirror QStringLiteral("Mirror") -#define KXMLQLCRGBMatrixMirrorBlend QStringLiteral("MirrorBlend") -#define KXMLQLCRGBMatrixBrightness QStringLiteral("Brightness") - -#define KXMLQLCRGBMatrixBeatEffect QStringLiteral("BeatEffect") -#define KXMLQLCRGBMatrixBeatSelection QStringLiteral("BeatSelection") -#define KXMLQLCRGBMatrixBeatOrientation QStringLiteral("BeatOrientation") static const int RGBMatrixColorMask = 0x00FFFFFF; @@ -93,37 +78,15 @@ RGBMatrix::RGBMatrix(Doc *doc) , m_continuousPhase(0.0) , m_applyingStyleAttributes(false) , m_controlMode(RGBMatrix::ControlModeRgb) - , m_rotation(0) - , m_mirror(0) - , m_mirrorBlend(MirrorFlip) - , m_brightness(1.0) - , m_beatEffect(BeatEffectOff) - , m_beatSelection(BeatSelAllOnDownbeat) - , m_beatOrientation(BeatOrientRows) - , m_currentBeat(0) - , m_lastBeat(-1) - , m_randomSegment(0) { - // Phase 4: precomputed-map state starts empty. m_currentGeneration begins - // at 0; consumers only trust the precomputed map when ready==1, so the - // initial value of m_precomputedGeneration is harmless until first kick. - m_precomputedReady.storeRelease(0); - m_precomputedInFlight.storeRelease(0); - m_currentGeneration.storeRelease(0); - m_precomputedGeneration = 0; - m_precomputedStep = -1; - m_precomputedColor = 0; - m_precomputedAlgorithm = nullptr; - setName(tr("New RGB Matrix")); setDuration(500); m_rgbColors.fill(QColor(), RGBAlgorithmColorDisplayCount); - // Don't pre-set slot 0 to red — leave all slots invalid so scripts - // can detect "no user colors" via algo.hasUserColors. - - setAlgorithm(RGBAlgorithm::algorithm(doc, "Stripes")); + setColor(0, Qt::red); + /** Register the fixed attributes before loading an algorithm, so that + * the attributes exposed by a Script are always appended after them */ for (int i = 0; i < ColorAttributeCount; ++i) { registerAttribute(tr("Color %1").arg(i + 1), LastWins | Single, -1.0, 16777215.0, @@ -131,36 +94,16 @@ RGBMatrix::RGBMatrix(Doc *doc) } int algoCount = RGBAlgorithm::algorithms(doc).count(); - registerAttribute(tr("Pattern"), LastWins | Single, 0.0, algoCount > 0 ? algoCount - 1 : 0, algorithmIndex()); + registerAttribute(tr("Pattern"), LastWins | Single, 0.0, algoCount > 0 ? algoCount - 1 : 0, 0); - // Mark the PixelPlan dirty initially. It will be (re)built lazily on the - // first updateMapChannels() call. Connect to Doc signals so that fixture - // address/mode changes and group membership changes invalidate the cache. - m_pixelPlanDirty.storeRelease(1); - if (doc != NULL) - { - connect(doc, &Doc::fixtureChanged, - this, &RGBMatrix::invalidatePixelPlan, Qt::DirectConnection); - connect(doc, &Doc::fixtureRemoved, - this, &RGBMatrix::invalidatePixelPlan, Qt::DirectConnection); - connect(doc, &Doc::fixtureGroupChanged, - this, &RGBMatrix::invalidatePixelPlan, Qt::DirectConnection); - connect(doc, &Doc::fixtureGroupRemoved, - this, &RGBMatrix::invalidatePixelPlan, Qt::DirectConnection); - } + setAlgorithm(RGBAlgorithm::algorithm(doc, "Stripes")); } RGBMatrix::~RGBMatrix() { - // Phase 4: any async precompute task captures `this`. Wait for it to - // drain before we tear down. Bounded to ~one rgbMap duration (~10ms). - while (m_precomputedInFlight.loadAcquire() != 0) - QThread::yieldCurrentThread(); - //if (m_runAlgorithm != NULL) // delete m_runAlgorithm; - deferDeleteAlgorithm(m_algorithm); - m_algorithm = NULL; + delete m_algorithm; delete m_stepHandler; } @@ -190,7 +133,7 @@ void RGBMatrix::setTotalDuration(quint32 msec) if (grp == NULL) return; - int steps = m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)); + int steps = m_algorithm->rgbMapStepCount(grp->size()); setDuration(msec / steps); } @@ -206,13 +149,12 @@ quint32 RGBMatrix::totalDuration() return 0; //qDebug () << "Algorithm steps:" << m_algorithm->rgbMapStepCount(grp->size()); - return m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)) * duration(); + return m_algorithm->rgbMapStepCount(grp->size()) * duration(); } void RGBMatrix::setDimmerControl(bool dimmerControl) { m_dimmerControl = dimmerControl; - m_pixelPlanDirty.storeRelease(1); } bool RGBMatrix::dimmerControl() const @@ -262,13 +204,6 @@ bool RGBMatrix::copyFrom(const Function* function) setAlgorithm(NULL); setControlMode(mtx->controlMode()); - setRotation(mtx->rotation()); - setMirror(mtx->mirror()); - setMirrorBlend(mtx->mirrorBlend()); - setBrightness(mtx->brightness()); - setBeatEffect(mtx->beatEffect()); - setBeatSelection(mtx->beatSelection()); - setBeatOrientation(mtx->beatOrientation()); return Function::copyFrom(function); } @@ -290,10 +225,6 @@ void RGBMatrix::setFixtureGroup(quint32 id) m_group = doc()->fixtureGroup(m_fixtureGroupID); } m_stepsCount = algorithmStepsCount(); - m_pixelPlanDirty.storeRelease(1); - // Phase 4: any precomputed map is for the OLD group. Invalidate. - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); } QList RGBMatrix::components() const @@ -310,18 +241,15 @@ QList RGBMatrix::components() const void RGBMatrix::setAlgorithm(RGBAlgorithm *algo) { - RGBAlgorithm *oldAlgo = nullptr; { QMutexLocker algorithmLocker(&m_algorithmMutex); - oldAlgo = m_algorithm; - m_algorithm = algo; - // Phase 4: invalidate any pending/stored precomputed frame BEFORE - // releasing the algorithm pointer for deletion. Bumping the generation - // ensures any in-flight JSThread task that captured the old generation - // will discard its result rather than store it. - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); + /** Unregister the attributes of the outgoing algorithm while it is + * still around, since their names come from its properties */ + unregisterScriptPropertyAttributes(); + + delete m_algorithm; + m_algorithm = algo; m_requestEngineCreation = true; @@ -343,16 +271,16 @@ void RGBMatrix::setAlgorithm(RGBAlgorithm *algo) } } - // Colors are injected via injectColors() at render time; - // no need to seed m_rgbColors from the script here. + QVector colors = script->rgbMapGetColors(); + for (int i = 0; i < colors.count(); i++) + m_rgbColors.replace(i, QColor::fromRgb(colors.at(i))); } } - // Phase 4: deferred delete of the previous algorithm. For Script-typed - // algos, this serializes after any pending precompute tasks on the - // JSThread (FIFO), avoiding use-after-free in the async path. - deferDeleteAlgorithm(oldAlgo); m_stepsCount = algorithmStepsCount(); + /** Expose the properties of the new algorithm as Function attributes */ + registerScriptPropertyAttributes(); + if (m_applyingStyleAttributes == false) Function::adjustAttribute(algorithmIndex(), PatternAttr); @@ -391,7 +319,7 @@ int RGBMatrix::algorithmStepsCount() FixtureGroup *grp = doc()->fixtureGroup(fixtureGroup()); if (grp != NULL) - return m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)); + return m_algorithm->rgbMapStepCount(grp->size()); return 0; } @@ -407,14 +335,8 @@ void RGBMatrix::previewMap(int step, RGBMatrixStep *handler) if (m_group != NULL) { - QSize algoSize = effectiveAlgorithmSize(m_group); - if (m_algorithm->usesAudio()) - m_algorithm->setDisplaySize(m_group->size()); setMapColors(m_algorithm); - m_algorithm->rgbMap(algoSize, handler->stepColor().rgb(), step, handler->m_map); - if (m_rotation || m_mirror) - applyTransforms(handler->m_map, algoSize, m_group->size(), - m_rotation, m_mirror, m_mirrorBlend); + m_algorithm->rgbMap(m_group->size(), handler->stepColor().rgb(), step, handler->m_map); } } @@ -513,6 +435,10 @@ void RGBMatrix::setProperty(QString propName, QString value) { RGBScript *script = static_cast (m_algorithm); script->setProperty(propName, value); + + QVector colors = script->rgbMapGetColors(); + for (int i = 0; i < colors.count(); i++) + setColor(i, QColor::fromRgb(colors.at(i))); } m_stepsCount = algorithmStepsCount(); @@ -635,34 +561,6 @@ bool RGBMatrix::loadXML(QXmlStreamReader &root) { setDimmerControl(root.readElementText().toInt()); } - else if (root.name() == KXMLQLCRGBMatrixRotation) - { - setRotation(root.readElementText().toInt()); - } - else if (root.name() == KXMLQLCRGBMatrixMirror) - { - setMirror(root.readElementText().toInt()); - } - else if (root.name() == KXMLQLCRGBMatrixMirrorBlend) - { - setMirrorBlend(stringToMirrorBlend(root.readElementText())); - } - else if (root.name() == KXMLQLCRGBMatrixBrightness) - { - setBrightness(root.readElementText().toDouble()); - } - else if (root.name() == KXMLQLCRGBMatrixBeatEffect) - { - setBeatEffect(stringToBeatEffect(root.readElementText())); - } - else if (root.name() == KXMLQLCRGBMatrixBeatSelection) - { - setBeatSelection(stringToBeatSelection(root.readElementText())); - } - else if (root.name() == KXMLQLCRGBMatrixBeatOrientation) - { - setBeatOrientation(stringToBeatOrientation(root.readElementText())); - } else { qWarning() << Q_FUNC_INFO << "Unknown RGB matrix tag:" << root.name(); @@ -732,24 +630,6 @@ bool RGBMatrix::saveXML(QXmlStreamWriter *doc) const doc->writeEndElement(); } - /* Rotation & Mirror */ - if (m_rotation != 0) - doc->writeTextElement(KXMLQLCRGBMatrixRotation, QString::number(m_rotation)); - if (m_mirror != 0) - doc->writeTextElement(KXMLQLCRGBMatrixMirror, QString::number(m_mirror)); - if (m_mirrorBlend != MirrorFlip) - doc->writeTextElement(KXMLQLCRGBMatrixMirrorBlend, mirrorBlendToString(m_mirrorBlend)); - if (m_brightness != 1.0) - doc->writeTextElement(KXMLQLCRGBMatrixBrightness, QString::number(m_brightness)); - - /* Beat Transform */ - if (m_beatEffect != BeatEffectOff) - { - doc->writeTextElement(KXMLQLCRGBMatrixBeatEffect, beatEffectToString(m_beatEffect)); - doc->writeTextElement(KXMLQLCRGBMatrixBeatSelection, beatSelectionToString(m_beatSelection)); - doc->writeTextElement(KXMLQLCRGBMatrixBeatOrientation, beatOrientationToString(m_beatOrientation)); - } - /* End the tag */ doc->writeEndElement(); @@ -825,19 +705,13 @@ void RGBMatrix::preRun(MasterTimer *timer) m_roundTime.restart(); - // The resolved fixture group may have changed since last preRun; force the - // PixelPlan to be rebuilt on the first updateMapChannels() call. - m_pixelPlanDirty.storeRelease(1); - - // Phase 4: drop any precomputed map carried over from a previous run. - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); - Function::preRun(timer); } void RGBMatrix::write(MasterTimer *timer, QList universes) { + Q_UNUSED(timer); + { QMutexLocker algorithmLocker(&m_algorithmMutex); if (m_group == NULL) @@ -858,131 +732,60 @@ void RGBMatrix::write(MasterTimer *timer, QList universes) if (m_runAlgorithm == NULL || m_runAlgorithm->apiVersion() == 0) return; - if (isPaused()) - return; + if (isPaused() == false) + { + // Get a new map every time elapsed is reset to zero + if (elapsed() < MasterTimer::tick()) + { + if (tempoType() == Beats) + m_stepBeatDuration = beatsToTime(duration(), timer->beatTimeDuration()); - // Refresh beat duration before any beat checks - if (tempoType() == Beats) - m_stepBeatDuration = beatsToTime(duration(), timer->beatTimeDuration()); + //qDebug() << "RGBMatrix step" << m_stepHandler->currentStepIndex() << ", color:" << QString::number(m_stepHandler->stepColor().rgb(), 16); + m_runAlgorithm->rgbMap(m_group->size(), m_stepHandler->stepColor().rgb(), + m_stepHandler->currentStepIndex(), m_stepHandler->m_map); + updateMapChannels(m_stepHandler->m_map, m_group, universes); + } + } + } - // Advance step if due - // Save pre-increment elapsed for the map-compute guard below - quint32 prevElapsed = elapsed(); + if (isPaused() == false) + { + // Increment the ms elapsed time incrementElapsed(); - bool stepChanged = false; + /* Check if we need to change direction, stop completely or go to next step + * The cases are: + * 1- time tempo type: act normally, on ms elapsed time + * 2- beat tempo type, beat occurred: check if the elapsed beats is a multiple of + * the step beat duration. If so, proceed to the next step + * 3- beat tempo type, not beat: if the ms elapsed time reached the step beat + * duration in ms, and the ms time to the next beat is not less than 1/16 of + * the step beat duration in ms, then proceed to the next step. If the ms time to the + * next beat is less than 1/16 of the step beat duration in ms, then defer the step + * change to case #2, to resync the matrix to the next beat + */ if (tempoType() == Time && elapsed() >= duration()) { - stepChanged = roundCheckLocked(); + roundCheck(); } else if (tempoType() == Beats) { if (timer->isBeat()) { incrementElapsedBeats(); + qDebug() << "Elapsed beats:" << elapsedBeats() << ", time elapsed:" << elapsed() << ", step time:" << m_stepBeatDuration; if (elapsedBeats() % duration() == 0) { - stepChanged = roundCheckLocked(); + roundCheck(); resetElapsed(); } } else if (elapsed() >= m_stepBeatDuration && (uint)timer->timeToNextBeat() > m_stepBeatDuration / 16) { - stepChanged = roundCheckLocked(); + qDebug() << "Elapsed exceeded"; + roundCheck(); } } - - // --- Beat Transform: update current beat --- - int beatsPerBar = 4; - if (m_beatEffect != BeatEffectOff) - { - AudioProfile *profile = doc()->audioProfile(doc()->activeAudioProfileId()); - if (profile == NULL) - profile = doc()->defaultAudioProfile(); - AudioChannel *channel = (profile != NULL) ? profile->channel() : NULL; - if (profile != NULL) - beatsPerBar = std::clamp(profile->channelConfig().aubio.beatsPerBar, 1, 8); - if (channel != NULL) - { - AudioSnapshot snap = channel->snapshot(); - if (snap.music.barPhase > 0) - m_currentBeat = int(snap.music.barPhase) % beatsPerBar; - else if (timer->isBeat()) - m_currentBeat = (m_currentBeat + 1) % beatsPerBar; - } - else if (timer->isBeat()) - { - m_currentBeat = (m_currentBeat + 1) % beatsPerBar; - } - - // Latch random segment on beat change - if (m_beatSelection == BeatSelRandom && m_currentBeat != m_lastBeat) - { - m_randomSegment = QRandomGenerator::global()->bounded(beatsPerBar); - m_lastBeat = m_currentBeat; - } - } - - // Compute and output map - // Recompute when: step just changed, first tick of a step, audio-reactive, or beat transform active - if (stepChanged || prevElapsed < MasterTimer::tick() || m_runAlgorithm->usesAudio() - || m_beatEffect != BeatEffectOff) - { - QSize algoSize = effectiveAlgorithmSize(m_group); - uint stepColor = m_stepHandler->stepColor().rgb(); - int stepIndex = m_stepHandler->currentStepIndex(); - quint32 generation = m_currentGeneration.loadAcquire(); - - // --- Phase 4: try to consume a precomputed frame --- - // - // The precomputed map already has rotation/mirror applied (those - // are stable across ticks). Beat transform is NOT precomputed - // because m_currentBeat is determined per-tick on this thread. - bool mapReady = false; - if (m_precomputedReady.loadAcquire() == 1) - { - QMutexLocker pre(&m_precomputedMutex); - if (m_precomputedReady.loadAcquire() == 1 - && m_precomputedGeneration == generation - && m_precomputedAlgorithm == m_runAlgorithm - && m_precomputedStep == stepIndex - && m_precomputedColor == stepColor - && m_precomputedAlgoSize == algoSize) - { - m_stepHandler->m_map = std::move(m_precomputedMap); - m_precomputedMap = RGBMap(); - m_precomputedReady.storeRelease(0); - mapReady = true; - } - else - { - // Stale or mismatched: discard. - m_precomputedReady.storeRelease(0); - } - } - - if (!mapReady) - { - if (m_runAlgorithm->usesAudio()) - m_runAlgorithm->setDisplaySize(m_group->size()); - m_runAlgorithm->rgbMap(algoSize, stepColor, stepIndex, m_stepHandler->m_map); - if (m_rotation || m_mirror) - applyTransforms(m_stepHandler->m_map, algoSize, m_group->size(), - m_rotation, m_mirror, m_mirrorBlend); - } - - if (m_beatEffect != BeatEffectOff) - applyBeatTransform(m_stepHandler->m_map, m_currentBeat, beatsPerBar); - updateMapChannels(m_stepHandler->m_map, m_group, universes, timer->beatTimeDuration()); - - // --- Phase 4: kick off async pre-computation for the NEXT tick --- - // - // We assume the next tick will use the same step/color (true for - // most ticks: step changes only when elapsed >= duration). If the - // assumption is wrong, the consumer will detect the mismatch and - // fall back to the synchronous path. - kickAsyncRgbMap(m_runAlgorithm, algoSize, stepColor, stepIndex, generation); - } } } @@ -1021,17 +824,14 @@ void RGBMatrix::postRun(MasterTimer *timer, QList universes) Function::postRun(timer, universes); } -bool RGBMatrix::roundCheckLocked() +void RGBMatrix::roundCheck() { + QMutexLocker algorithmLocker(&m_algorithmMutex); if (m_algorithm == NULL) - return false; + return; - bool advanced = m_stepHandler->checkNextStep(runOrder(), m_rgbColors[0], m_rgbColors[1], m_stepsCount); - if (advanced == false) - { + if (m_stepHandler->checkNextStep(runOrder(), m_rgbColors[0], m_rgbColors[1], m_stepsCount) == false) stop(FunctionParent::master()); - return false; - } // Update continuous phase based on current step index (prevents cumulative rounding errors) // This is analogous to how EFX uses m_currentAngle for phase scaling @@ -1044,14 +844,6 @@ bool RGBMatrix::roundCheckLocked() roundElapsed(m_stepBeatDuration); else roundElapsed(duration()); - - return true; -} - -void RGBMatrix::roundCheck() -{ - QMutexLocker algorithmLocker(&m_algorithmMutex); - roundCheckLocked(); } QSharedPointer RGBMatrix::getFader(Universe *universe) @@ -1063,7 +855,7 @@ QSharedPointer RGBMatrix::getFader(Universe *universe) QSharedPointer fader = m_fadersMap.value(universe->id(), QSharedPointer()); if (fader.isNull()) { - fader = universe->requestFader(Universe::blendModePriority(blendMode())); + fader = universe->requestFader(); fader->adjustIntensity(getAttributeValue(Intensity)); fader->setBlendMode(blendMode()); fader->setName(name()); @@ -1074,7 +866,7 @@ QSharedPointer RGBMatrix::getFader(Universe *universe) return fader; } -void RGBMatrix::updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime, uint fadeOutTime) +void RGBMatrix::updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime) { fc.setStart(fc.current()); fc.setTarget(value); @@ -1082,383 +874,144 @@ void RGBMatrix::updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime, u fc.setReady(false); // fade in/out depends on target value if (value == 0) - fc.setFadeTime(fadeOutTime); + fc.setFadeTime(fadeOutSpeed()); else fc.setFadeTime(fadeTime); } -void RGBMatrix::invalidatePixelPlan(quint32 id) -{ - Q_UNUSED(id) - // Conservative: any fixture / fixture-group change may affect the resolved - // channels. Marking dirty is cheap; the rebuild only happens on next tick. - m_pixelPlanDirty.storeRelease(1); -} - -/***************************************************************************** - * Phase 4: Async rgbMap pre-computation helpers - *****************************************************************************/ - -void RGBMatrix::deferDeleteAlgorithm(RGBAlgorithm *algo) -{ - if (algo == nullptr) - return; - -#ifdef QT_QML_LIB - // Script-typed algorithms own QJSValue handles tied to the JSThread's - // QJSEngine. Even ignoring our async tasks, deleting them off-thread is - // unsafe. Queue the delete on the JSThread so it serializes after any - // pending precompute tasks (FIFO). - if (algo->type() == RGBAlgorithm::Script - && RGBScript::scheduleOnJSThread([algo]() { delete algo; })) - { - return; - } -#endif - delete algo; -} - -void RGBMatrix::kickAsyncRgbMap(RGBAlgorithm *algo, const QSize &algoSize, - uint stepColor, int step, quint32 generation) -{ -#ifdef QT_QML_LIB - if (algo == nullptr) - return; - - // Only Script algorithms benefit — others (Image, PlainColor, Text, Audio) - // run inline on this thread already and are fast. - if (algo->type() != RGBAlgorithm::Script) - return; - - // Throttle: at most one async task in flight per matrix. If a previous - // task hasn't been consumed yet, skip — the current frame's sync path is - // already producing usable output. - int expected = 0; - if (!m_precomputedInFlight.testAndSetAcquire(expected, 1)) - return; - - // Capture by value: algo pointer + immutable params + generation snapshot. - // We do NOT take m_algorithmMutex inside the JSThread task — that would - // deadlock with the synchronous rgbMap path (MasterTimer holds the mutex - // while BlockingQueuedConnection-waiting on JSThread). - // - // Lifetime safety: - // - algo pointer remains valid as long as any in-flight task could be - // using it: setAlgorithm() / ~RGBMatrix() use deferDeleteAlgorithm() - // which queues the delete on the JSThread *after* this task (FIFO). - // - `this` remains valid because ~RGBMatrix() spins on - // m_precomputedInFlight before destroying members. - QSize captureAlgoSize = algoSize; - int captureRotation = m_rotation; - int captureMirror = m_mirror; - MirrorBlend captureBlend = m_mirrorBlend; - QSize captureGroupSize; - if (m_group != nullptr) - captureGroupSize = m_group->size(); - bool captureUsesAudio = algo->usesAudio(); - - bool ok = RGBScript::scheduleOnJSThread( - [this, algo, captureAlgoSize, stepColor, step, generation, - captureRotation, captureMirror, captureBlend, captureGroupSize, - captureUsesAudio]() - { - // Re-check generation BEFORE we even call rgbMap. Cheap and lets - // us bail out if the matrix was reconfigured while we were queued. - if (m_currentGeneration.loadAcquire() != generation) - { - m_precomputedInFlight.storeRelease(0); - return; - } - - if (captureUsesAudio && !captureGroupSize.isEmpty()) - algo->setDisplaySize(captureGroupSize); - - RGBMap localMap; - algo->rgbMap(captureAlgoSize, stepColor, step, localMap); - - if ((captureRotation || captureMirror) && !captureGroupSize.isEmpty()) - applyTransforms(localMap, captureAlgoSize, captureGroupSize, - captureRotation, captureMirror, captureBlend); - - // Re-check generation AFTER computation. If a parameter changed - // mid-flight, drop the result rather than poison the cache. - if (m_currentGeneration.loadAcquire() != generation) - { - m_precomputedInFlight.storeRelease(0); - return; - } - - { - QMutexLocker pre(&m_precomputedMutex); - m_precomputedMap = std::move(localMap); - m_precomputedAlgoSize = captureAlgoSize; - m_precomputedStep = step; - m_precomputedColor = stepColor; - m_precomputedGeneration = generation; - m_precomputedAlgorithm = algo; - } - m_precomputedReady.storeRelease(1); - m_precomputedInFlight.storeRelease(0); - }); - - if (!ok) - m_precomputedInFlight.storeRelease(0); -#else - Q_UNUSED(algo) - Q_UNUSED(algoSize) - Q_UNUSED(stepColor) - Q_UNUSED(step) - Q_UNUSED(generation) -#endif -} - -void RGBMatrix::rebuildPixelPlan(const FixtureGroup *grp) +void RGBMatrix::updateMapChannels(const RGBMap& map, const FixtureGroup *grp, QList universes) { - m_pixelPlan.clear(); - m_pixelPlanGroup = grp; - m_pixelPlanDirty.storeRelease(0); - - if (grp == NULL) - return; - - Doc *d = doc(); + uint fadeTime = (overrideFadeInSpeed() == defaultSpeed()) ? fadeInSpeed() : overrideFadeInSpeed(); + // Create/modify fade channels for ALL heads in the group QMapIterator it(grp->headsMap()); while (it.hasNext()) { it.next(); - const QLCPoint &pt = it.key(); - const GroupHead &grpHead = it.value(); - - Fixture *fxi = d->fixture(grpHead.fxi); + QLCPoint pt = it.key(); + GroupHead grpHead = it.value(); + Fixture *fxi = doc()->fixture(grpHead.fxi); if (fxi == NULL) continue; QLCFixtureHead head = fxi->head(grpHead.head); - const quint32 absAddress = fxi->universeAddress(); - const quint32 fxID = grpHead.fxi; - const quint16 px = quint16(pt.x()); - const quint16 py = quint16(pt.y()); - - auto addEntry = [&](quint32 ch, PixelValueSource src) { - if (ch == QLCChannel::invalid()) - return; - PixelPlanEntry e; - e.x = px; - e.y = py; - e.universeIndex = quint32((absAddress + ch) / 512); - e.fixtureID = fxID; - e.channel = ch; - e.source = src; - m_pixelPlan.append(e); - }; + + if (pt.y() >= map.count() || pt.x() >= map[pt.y()].count()) + continue; + + uint col = map[pt.y()][pt.x()]; + QVector channelList; + QVector valueList; if (m_controlMode == ControlModeRgb) { - QVector chList = head.rgbChannels(); - if (chList.size() == 3) + channelList = head.rgbChannels(); + + if (channelList.size() == 3) { - addEntry(chList.at(0), VS_Red); - addEntry(chList.at(1), VS_Green); - addEntry(chList.at(2), VS_Blue); + valueList.append(qRed(col)); + valueList.append(qGreen(col)); + valueList.append(qBlue(col)); } else { - chList = head.cmyChannels(); - if (chList.size() == 3) + channelList = head.cmyChannels(); + + if (channelList.size() == 3) { - addEntry(chList.at(0), VS_Cyan); - addEntry(chList.at(1), VS_Magenta); - addEntry(chList.at(2), VS_Yellow); + // CMY color mixing + QColor cmyCol(col); + valueList.append(cmyCol.cyan()); + valueList.append(cmyCol.magenta()); + valueList.append(cmyCol.yellow()); } } } - else if (m_controlMode == ControlModeRgbw || m_controlMode == ControlModeRgbwBrighter) + else if (m_controlMode == ControlModeShutter) { - const bool subtractWhite = (m_controlMode == ControlModeRgbw); - quint32 rCh = head.channelNumber(QLCChannel::Red, QLCChannel::MSB); - quint32 gCh = head.channelNumber(QLCChannel::Green, QLCChannel::MSB); - quint32 bCh = head.channelNumber(QLCChannel::Blue, QLCChannel::MSB); - quint32 wCh = head.channelNumber(QLCChannel::White, QLCChannel::MSB); + channelList = head.shutterChannels(); - if (rCh != QLCChannel::invalid() && gCh != QLCChannel::invalid() && bCh != QLCChannel::invalid()) + if (channelList.size()) { - addEntry(rCh, subtractWhite ? VS_RedSubW : VS_Red); - addEntry(gCh, subtractWhite ? VS_GreenSubW : VS_Green); - addEntry(bCh, subtractWhite ? VS_BlueSubW : VS_Blue); - - if (wCh != QLCChannel::invalid()) - addEntry(wCh, VS_White); - - if (m_dimmerControl) - { - quint32 masterDim = fxi->masterIntensityChannel(); - quint32 headDim = head.channelNumber(QLCChannel::Intensity, QLCChannel::MSB); - - if (masterDim != QLCChannel::invalid()) - addEntry(masterDim, VS_Grey); - - if (headDim != QLCChannel::invalid() && headDim != masterDim) - addEntry(headDim, VS_GreyOrFull); - } + // make sure only one channel is in the list + channelList.resize(1); + valueList.append(rgbToGrey(col)); } } - else if (m_controlMode == ControlModeShutter) - { - QVector chList = head.shutterChannels(); - if (chList.size()) - addEntry(chList.first(), VS_Grey); - } else if (m_controlMode == ControlModeDimmer || m_dimmerControl) { + // Collect all dimmers that affect current head: + // They are the master dimmer (affects whole fixture) + // and per-head dimmer. + // + // If there are no RGB or CMY channels, the least important* dimmer channel + // is used to create grayscale image. + // + // The rest of the dimmer channels are set to full if dimmer control is + // enabled and target color is > 0 (see + // https://www.qlcplus.org/forum/viewtopic.php?f=29&t=11090) + // + // Note: If there is only one head, and only one dimmer channel, + // make it a master dimmer in fixture definition. + // + // *least important - per head dimmer if present, + // otherwise per fixture dimmer if present + quint32 masterDim = fxi->masterIntensityChannel(); quint32 headDim = head.channelNumber(QLCChannel::Intensity, QLCChannel::MSB); if (masterDim != QLCChannel::invalid()) - addEntry(masterDim, VS_Grey); + { + channelList.append(masterDim); + valueList.append(rgbToGrey(col)); + } if (headDim != QLCChannel::invalid() && headDim != masterDim) - addEntry(headDim, masterDim != QLCChannel::invalid() ? VS_GreyOrFull : VS_Grey); + { + channelList.append(headDim); + // If a master dimmer is present, it carries the greyscale fade and the + // per-head dimmer is just opened fully (on/off). With no master dimmer + // (e.g. generic single-channel dimmers), the head dimmer must carry the + // greyscale value itself, otherwise it would only ever output 0 or 255. + if (masterDim != QLCChannel::invalid()) + valueList.append(rgbToGrey(col) == 0 ? 0 : 255); + else + valueList.append(rgbToGrey(col)); + } } else { - quint32 ch = QLCChannel::invalid(); if (m_controlMode == ControlModeWhite) - ch = head.channelNumber(QLCChannel::White, QLCChannel::MSB); + channelList.append(head.channelNumber(QLCChannel::White, QLCChannel::MSB)); else if (m_controlMode == ControlModeAmber) - ch = head.channelNumber(QLCChannel::Amber, QLCChannel::MSB); + channelList.append(head.channelNumber(QLCChannel::Amber, QLCChannel::MSB)); else if (m_controlMode == ControlModeUV) - ch = head.channelNumber(QLCChannel::UV, QLCChannel::MSB); + channelList.append(head.channelNumber(QLCChannel::UV, QLCChannel::MSB)); - addEntry(ch, VS_Grey); + valueList.append(rgbToGrey(col)); } - } - // Sort by (universeIndex, y, x, channel) so the per-tick loop can amortize - // the getFader() lookup across consecutive entries from the same universe, - // and benefit from per-pixel scratch reuse within a universe. - std::sort(m_pixelPlan.begin(), m_pixelPlan.end(), - [](const PixelPlanEntry &a, const PixelPlanEntry &b) { - if (a.universeIndex != b.universeIndex) return a.universeIndex < b.universeIndex; - if (a.y != b.y) return a.y < b.y; - if (a.x != b.x) return a.x < b.x; - return a.channel < b.channel; - }); -} + quint32 absAddress = fxi->universeAddress(); -void RGBMatrix::updateMapChannels(const RGBMap& map, const FixtureGroup *grp, QList universes, int beatDuration) -{ - uint fadeTime = (overrideFadeInSpeed() == defaultSpeed()) ? fadeInSpeed() : overrideFadeInSpeed(); - uint fadeOutTime = (overrideFadeOutSpeed() == defaultSpeed()) ? fadeOutSpeed() : overrideFadeOutSpeed(); - - if (tempoType() == Beats) - { - fadeTime = beatsToTime(fadeTime, beatDuration); - fadeOutTime = beatsToTime(fadeOutTime, beatDuration); - } - - // Rebuild the pre-resolved channel plan whenever it has been invalidated - // (fixture group / control mode / dimmer control / fixture address change). - if (m_pixelPlanDirty.loadAcquire() || m_pixelPlanGroup != grp) - rebuildPixelPlan(grp); - - if (m_pixelPlan.isEmpty()) - return; - - Doc *d = doc(); - const int mapH = map.count(); - const int universeCount = universes.size(); - - quint32 lastUni = UINT_MAX; - Universe *cachedUniverse = nullptr; - QSharedPointer cachedFader; - - quint16 lastX = 0xFFFF; - quint16 lastY = 0xFFFF; - bool pixelValid = false; - uchar r = 0, g = 0, b = 0, w = 0, grey = 0; - QColor cmyCol; - - for (int i = 0, n = m_pixelPlan.size(); i < n; ++i) - { - const PixelPlanEntry &e = m_pixelPlan.at(i); - - // Per-pixel scratch values. Recomputed only when (x, y) changes, - // which (after sorting) happens once per pixel within a universe. - if (e.x != lastX || e.y != lastY) + for (int i = 0; i < channelList.count(); i++) { - lastX = e.x; - lastY = e.y; - pixelValid = (int(e.y) < mapH && int(e.x) < map.at(e.y).count()); - if (pixelValid) + if (channelList.at(i) == QLCChannel::invalid()) + continue; + + quint32 universeIndex = floor((absAddress + channelList.at(i)) / 512); + Universe *universe = universes.at(universeIndex); + QSharedPointer fader = getFader(universe); + if (fader.isNull()) + continue; + + const quint32 fixtureID = grpHead.fxi; + const quint32 channel = channelList.at(i); + const uchar value = valueList.at(i); + fader->updateChannel(doc(), universe, fixtureID, channel, [this, value, fadeTime](FadeChannel &fc) { - uint col = map.at(e.y).at(e.x); - r = uchar(qRed(col)); - g = uchar(qGreen(col)); - b = uchar(qBlue(col)); - w = qMin(r, qMin(g, b)); - grey = rgbToGrey(col); - cmyCol = QColor::fromRgb(col); - - if (m_brightness != 1.0) - { - r = uchar(qBound(0, int(r * m_brightness), 255)); - g = uchar(qBound(0, int(g * m_brightness), 255)); - b = uchar(qBound(0, int(b * m_brightness), 255)); - w = uchar(qBound(0, int(w * m_brightness), 255)); - grey = uchar(qBound(0, int(grey * m_brightness), 255)); - cmyCol = QColor::fromRgb(qBound(0, int(qRed(cmyCol.rgb()) * m_brightness), 255), - qBound(0, int(qGreen(cmyCol.rgb()) * m_brightness), 255), - qBound(0, int(qBlue(cmyCol.rgb()) * m_brightness), 255)); - } - } - } - - if (!pixelValid) - continue; - - // Universe / fader lookup is amortized across consecutive entries from - // the same universe thanks to the sort in rebuildPixelPlan(). - if (e.universeIndex != lastUni) - { - lastUni = e.universeIndex; - if (int(e.universeIndex) >= universeCount) - { - cachedUniverse = nullptr; - cachedFader.clear(); - } - else - { - cachedUniverse = universes.at(e.universeIndex); - cachedFader = getFader(cachedUniverse); - } - } - - if (cachedFader.isNull()) - continue; - - uchar value = 0; - switch (e.source) - { - case VS_Red: value = r; break; - case VS_Green: value = g; break; - case VS_Blue: value = b; break; - case VS_Cyan: value = uchar(cmyCol.cyan()); break; - case VS_Magenta: value = uchar(cmyCol.magenta()); break; - case VS_Yellow: value = uchar(cmyCol.yellow()); break; - case VS_White: value = w; break; - case VS_RedSubW: value = uchar(r - w); break; - case VS_GreenSubW: value = uchar(g - w); break; - case VS_BlueSubW: value = uchar(b - w); break; - case VS_Grey: value = grey; break; - case VS_GreyOrFull: value = grey == 0 ? 0 : 255; break; + updateFaderValues(fc, value, fadeTime); + }); } - - cachedFader->updateChannel(d, cachedUniverse, e.fixtureID, e.channel, - [this, value, fadeTime, fadeOutTime](FadeChannel &fc) - { - updateFaderValues(fc, value, fadeTime, fadeOutTime); - }); } } @@ -1502,6 +1055,10 @@ int RGBMatrix::adjustAttribute(qreal fraction, int attributeId) { applyPatternAttribute(getAttributeValue(PatternAttr)); } + else if (attrIndex >= ScriptPropertyAttr) + { + applyScriptPropertyAttribute(attrIndex - ScriptPropertyAttr, getAttributeValue(attrIndex)); + } return attrIndex; } @@ -1512,6 +1069,109 @@ void RGBMatrix::applyStyleAttributes() applyColorAttribute(i, getAttributeValue(Color1Attr + i)); applyPatternAttribute(getAttributeValue(PatternAttr)); + + for (int i = 0; i < scriptPropertyAttributes().count(); i++) + applyScriptPropertyAttribute(i, getAttributeValue(ScriptPropertyAttr + i)); +} + +QList RGBMatrix::scriptPropertyAttributes() const +{ + QList list; + + if (m_algorithm == NULL || m_algorithm->type() != RGBAlgorithm::Script) + return list; + + RGBScript *script = static_cast (m_algorithm); + + foreach (RGBScriptProperty prop, script->properties()) + { + /** Only the properties that a slider can drive are exposed: + * a list needs at least two values to pick from, a range needs + * a meaningful span and a string cannot be controlled at all */ + if (prop.m_type == RGBScriptProperty::List) + { + if (prop.m_listValues.count() > 1) + list.append(prop); + } + else if (prop.m_type == RGBScriptProperty::Range) + { + if (prop.m_rangeMaxValue > prop.m_rangeMinValue) + list.append(prop); + } + else if (prop.m_type == RGBScriptProperty::Float) + { + list.append(prop); + } + } + + return list; +} + +QString RGBMatrix::scriptPropertyAttributeName(const RGBScriptProperty &prop) +{ + return prop.m_displayName.isEmpty() ? prop.m_name : prop.m_displayName; +} + +void RGBMatrix::unregisterScriptPropertyAttributes() +{ + foreach (RGBScriptProperty prop, scriptPropertyAttributes()) + unregisterAttribute(scriptPropertyAttributeName(prop)); +} + +void RGBMatrix::registerScriptPropertyAttributes() +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + + foreach (RGBScriptProperty prop, scriptPropertyAttributes()) + { + qreal min = 0.0; + qreal max = 1.0; + + if (prop.m_type == RGBScriptProperty::List) + max = prop.m_listValues.count() - 1; + else if (prop.m_type == RGBScriptProperty::Range) + { + min = prop.m_rangeMinValue; + max = prop.m_rangeMaxValue; + } + /** Scripts don't declare a range for float properties, + * so 0.0 - 1.0 is assumed */ + + /** A list property holds a string, so the attribute value is the + * index of the current value in the values list */ + QString current = property(prop.m_name); + qreal value = prop.m_type == RGBScriptProperty::List ? + qMax(0, prop.m_listValues.indexOf(current)) : current.toDouble(); + + registerAttribute(scriptPropertyAttributeName(prop), LastWins | Single, + min, max, qBound(min, value, max)); + } +} + +void RGBMatrix::applyScriptPropertyAttribute(int attrIndex, qreal value) +{ + QList props = scriptPropertyAttributes(); + if (attrIndex < 0 || attrIndex >= props.count()) + return; + + const RGBScriptProperty prop = props.at(attrIndex); + QString strValue; + + switch (prop.m_type) + { + case RGBScriptProperty::List: + strValue = prop.m_listValues.at(qBound(0, int(qRound(value)), prop.m_listValues.count() - 1)); + break; + case RGBScriptProperty::Float: + strValue = QString::number(value); + break; + default: + strValue = QString::number(qRound(value)); + break; + } + + if (property(prop.m_name) != strValue) + setProperty(prop.m_name, strValue); } void RGBMatrix::applyColorAttribute(int colorIndex, qreal packedColor) @@ -1595,34 +1255,23 @@ RGBMatrix::ControlMode RGBMatrix::controlMode() const void RGBMatrix::setControlMode(RGBMatrix::ControlMode mode) { m_controlMode = mode; - m_pixelPlanDirty.storeRelease(1); - // Phase 4: control mode affects how rgbMap output is interpreted, but - // not the rgbMap output itself. Bump generation defensively to avoid any - // edge case where the script could observe controlMode (it currently - // doesn't, but better safe than sorry). - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); emit changed(id()); } RGBMatrix::ControlMode RGBMatrix::stringToControlMode(QString mode) { - if (mode.compare(KXMLQLCRGBMatrixControlModeRgb, Qt::CaseInsensitive) == 0) + if (mode == KXMLQLCRGBMatrixControlModeRgb) return ControlModeRgb; - else if (mode.compare(KXMLQLCRGBMatrixControlModeAmber, Qt::CaseInsensitive) == 0) + else if (mode == KXMLQLCRGBMatrixControlModeAmber) return ControlModeAmber; - else if (mode.compare(KXMLQLCRGBMatrixControlModeWhite, Qt::CaseInsensitive) == 0) + else if (mode == KXMLQLCRGBMatrixControlModeWhite) return ControlModeWhite; - else if (mode.compare(KXMLQLCRGBMatrixControlModeUV, Qt::CaseInsensitive) == 0) + else if (mode == KXMLQLCRGBMatrixControlModeUV) return ControlModeUV; - else if (mode.compare(KXMLQLCRGBMatrixControlModeDimmer, Qt::CaseInsensitive) == 0) + else if (mode == KXMLQLCRGBMatrixControlModeDimmer) return ControlModeDimmer; - else if (mode.compare(KXMLQLCRGBMatrixControlModeShutter, Qt::CaseInsensitive) == 0) + else if (mode == KXMLQLCRGBMatrixControlModeShutter) return ControlModeShutter; - else if (mode.compare(KXMLQLCRGBMatrixControlModeRgbw, Qt::CaseInsensitive) == 0) - return ControlModeRgbw; - else if (mode.compare(KXMLQLCRGBMatrixControlModeRgbwBrighter, Qt::CaseInsensitive) == 0) - return ControlModeRgbwBrighter; return ControlModeRgb; } @@ -1650,420 +1299,6 @@ QString RGBMatrix::controlModeToString(RGBMatrix::ControlMode mode) case ControlModeShutter: return QString(KXMLQLCRGBMatrixControlModeShutter); break; - case ControlModeRgbw: - return QString(KXMLQLCRGBMatrixControlModeRgbw); - break; - case ControlModeRgbwBrighter: - return QString(KXMLQLCRGBMatrixControlModeRgbwBrighter); - break; - } -} - -/************************************************************************* - * Rotation & Mirror - *************************************************************************/ - -int RGBMatrix::rotation() const -{ - return m_rotation; -} - -void RGBMatrix::setRotation(int r) -{ - m_rotation = r & 3; - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); - emit changed(id()); -} - -int RGBMatrix::mirror() const -{ - return m_mirror; -} - -void RGBMatrix::setMirror(int m) -{ - m_mirror = m & 3; - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); - emit changed(id()); -} - -RGBMatrix::MirrorBlend RGBMatrix::mirrorBlend() const -{ - return m_mirrorBlend; -} - -void RGBMatrix::setMirrorBlend(MirrorBlend b) -{ - if (b < MirrorFlip || b > MirrorAdditive) - b = MirrorFlip; - m_mirrorBlend = b; - m_currentGeneration.fetchAndAddRelaxed(1); - m_precomputedReady.storeRelease(0); - emit changed(id()); -} - -qreal RGBMatrix::brightness() const -{ - return m_brightness; -} - -void RGBMatrix::setBrightness(qreal b) -{ - m_brightness = qMax(0.0, b); - emit changed(id()); -} - -QSize RGBMatrix::effectiveAlgorithmSize() const -{ - if (m_group == NULL) - return QSize(); - return effectiveAlgorithmSize(m_group); -} - -QSize RGBMatrix::effectiveAlgorithmSize(const FixtureGroup *grp) const -{ - QSize s = grp->size(); - if (m_rotation == 1 || m_rotation == 3) - s = QSize(s.height(), s.width()); - return s; -} - -QString RGBMatrix::mirrorBlendToString(MirrorBlend b) -{ - switch (b) - { - case MirrorMax: return QStringLiteral("Max"); - case MirrorAverage: return QStringLiteral("Average"); - case MirrorAdditive: return QStringLiteral("Additive"); - default: return QStringLiteral("Flip"); - } -} - -RGBMatrix::MirrorBlend RGBMatrix::stringToMirrorBlend(const QString &s) -{ - if (s.compare(QStringLiteral("Max"), Qt::CaseInsensitive) == 0) return MirrorMax; - if (s.compare(QStringLiteral("Average"), Qt::CaseInsensitive) == 0) return MirrorAverage; - if (s.compare(QStringLiteral("Additive"), Qt::CaseInsensitive) == 0) return MirrorAdditive; - return MirrorFlip; -} - -static inline uint blendPixels(uint a, uint b, RGBMatrix::MirrorBlend blend) -{ - uint ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF; - uint br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF; - uint r, g, bl; - - switch (blend) - { - case RGBMatrix::MirrorMax: - r = qMax(ar, br); g = qMax(ag, bg); bl = qMax(ab, bb); - break; - case RGBMatrix::MirrorAverage: - r = (ar + br) / 2; g = (ag + bg) / 2; bl = (ab + bb) / 2; - break; - case RGBMatrix::MirrorAdditive: - r = qMin(255u, ar + br); g = qMin(255u, ag + bg); bl = qMin(255u, ab + bb); - break; - default: // MirrorFlip — caller handles this case directly - return a; - } - return (r << 16) | (g << 8) | bl; -} - -void RGBMatrix::applyTransforms(RGBMap &map, const QSize & /* srcSize */, const QSize &dstSize, - int rotation, int mirror, MirrorBlend blend) -{ - int dw = dstSize.width(); - int dh = dstSize.height(); - - // Source dimensions from the actual map data - int sh = map.size(); - int sw = (sh > 0) ? map[0].size() : 0; - - // Step 1: Rotation — build a new map with destination dimensions - RGBMap rotated; - rotated.resize(dh); - for (int y = 0; y < dh; y++) - { - rotated[y].resize(dw); - rotated[y].fill(0); - } - - for (int dy = 0; dy < dh; dy++) - { - for (int dx = 0; dx < dw; dx++) - { - int sx, sy; - switch (rotation) - { - case 0: // 0°: identity - sx = dx; sy = dy; - break; - case 1: // 90° CW - sx = dy; sy = sh - 1 - dx; - break; - case 2: // 180° - sx = dw - 1 - dx; sy = dh - 1 - dy; - break; - case 3: // 270° CW - sx = sw - 1 - dy; sy = dx; - break; - default: - sx = dx; sy = dy; - break; - } - - if (sy >= 0 && sy < sh && sx >= 0 && sx < (int)map[sy].size()) - rotated[dy][dx] = map[sy][sx]; - } - } - - // Step 2: Mirror - if (mirror & 1) // Horizontal — mirror placed on vertical center line - { - if (blend == MirrorFlip) - { - // Pure flip: left half is source, copied to right half - for (int y = 0; y < dh; y++) - for (int x = 0; x < dw / 2; x++) - rotated[y][dw - 1 - x] = rotated[y][x]; - } - else - { - for (int y = 0; y < dh; y++) - { - for (int x = 0; x < dw / 2; x++) - { - int mx = dw - 1 - x; - uint merged = blendPixels(rotated[y][x], rotated[y][mx], blend); - rotated[y][x] = merged; - rotated[y][mx] = merged; - } - } - } - } - - if (mirror & 2) // Vertical — mirror placed on horizontal center line - { - if (blend == MirrorFlip) - { - // Pure flip: top half is source, copied to bottom half - for (int y = 0; y < dh / 2; y++) - for (int x = 0; x < dw; x++) - rotated[dh - 1 - y][x] = rotated[y][x]; - } - else - { - for (int y = 0; y < dh / 2; y++) - { - int my = dh - 1 - y; - for (int x = 0; x < dw; x++) - { - uint merged = blendPixels(rotated[y][x], rotated[my][x], blend); - rotated[y][x] = merged; - rotated[my][x] = merged; - } - } - } - } - - map = rotated; -} - -/************************************************************************* - * Beat Transform - *************************************************************************/ - -RGBMatrix::BeatEffect RGBMatrix::beatEffect() const -{ - return m_beatEffect; -} - -void RGBMatrix::setBeatEffect(BeatEffect e) -{ - m_beatEffect = e; -} - -RGBMatrix::BeatSelection RGBMatrix::beatSelection() const -{ - return m_beatSelection; -} - -void RGBMatrix::setBeatSelection(BeatSelection s) -{ - m_beatSelection = s; -} - -RGBMatrix::BeatOrientation RGBMatrix::beatOrientation() const -{ - return m_beatOrientation; -} - -void RGBMatrix::setBeatOrientation(BeatOrientation o) -{ - m_beatOrientation = o; -} - -QString RGBMatrix::beatEffectToString(BeatEffect e) -{ - switch (e) - { - case BeatEffectMirror: return QStringLiteral("Mirror"); - case BeatEffectColorInvert: return QStringLiteral("ColorInvert"); - case BeatEffectBlackout: return QStringLiteral("Blackout"); - case BeatEffectWhiteout: return QStringLiteral("Whiteout"); - default: return QStringLiteral("Off"); - } -} - -RGBMatrix::BeatEffect RGBMatrix::stringToBeatEffect(const QString &s) -{ - if (s.compare(QStringLiteral("Mirror"), Qt::CaseInsensitive) == 0) return BeatEffectMirror; - if (s.compare(QStringLiteral("ColorInvert"), Qt::CaseInsensitive) == 0) return BeatEffectColorInvert; - if (s.compare(QStringLiteral("Blackout"), Qt::CaseInsensitive) == 0) return BeatEffectBlackout; - if (s.compare(QStringLiteral("Whiteout"), Qt::CaseInsensitive) == 0) return BeatEffectWhiteout; - return BeatEffectOff; -} - -QString RGBMatrix::beatSelectionToString(BeatSelection s) -{ - switch (s) - { - case BeatSelWalk: return QStringLiteral("Walk"); - case BeatSelRandom: return QStringLiteral("Random"); - default: return QStringLiteral("AllOnDownbeat"); - } -} - -RGBMatrix::BeatSelection RGBMatrix::stringToBeatSelection(const QString &s) -{ - if (s.compare(QStringLiteral("Walk"), Qt::CaseInsensitive) == 0) return BeatSelWalk; - if (s.compare(QStringLiteral("Random"), Qt::CaseInsensitive) == 0) return BeatSelRandom; - return BeatSelAllOnDownbeat; -} - -QString RGBMatrix::beatOrientationToString(BeatOrientation o) -{ - switch (o) - { - case BeatOrientColumns: return QStringLiteral("Columns"); - default: return QStringLiteral("Rows"); - } -} - -RGBMatrix::BeatOrientation RGBMatrix::stringToBeatOrientation(const QString &s) -{ - if (s.compare(QStringLiteral("Columns"), Qt::CaseInsensitive) == 0) return BeatOrientColumns; - return BeatOrientRows; -} - -void RGBMatrix::segmentRange(int segment, int total, int segmentsCount, int &start, int &end) -{ - if (segmentsCount < 1) segmentsCount = 1; - int base = total / segmentsCount; - int remainder = total % segmentsCount; - start = segment * base + qMin(segment, remainder); - end = start + base + (segment < remainder ? 1 : 0); -} - -void RGBMatrix::applyBeatTransform(RGBMap &map, int currentBeat, int beatsPerBar) -{ - if (m_beatEffect == BeatEffectOff) - return; - - int rows = map.size(); - if (rows == 0) return; - int cols = map[0].size(); - if (cols == 0) return; - - int total = (m_beatOrientation == BeatOrientRows) ? rows : cols; - if (beatsPerBar < 1) beatsPerBar = 1; - - // Determine affected segments - QVector affected; - if (m_beatSelection == BeatSelAllOnDownbeat) - { - if (currentBeat != beatsPerBar - 1) return; - affected.reserve(beatsPerBar); - for (int i = 0; i < beatsPerBar; i++) affected.append(i); - } - else if (m_beatSelection == BeatSelWalk) - { - affected = {currentBeat % beatsPerBar}; - } - else // BeatSelRandom - { - affected = {m_randomSegment % beatsPerBar}; - } - - for (int seg : affected) - { - int start, end; - segmentRange(seg, total, beatsPerBar, start, end); - - switch (m_beatEffect) - { - case BeatEffectMirror: - if (m_beatOrientation == BeatOrientRows) - { - for (int y = start; y < end; y++) - std::reverse(map[y].begin(), map[y].end()); - } - else - { - for (int y = 0; y < rows; y++) - std::reverse(map[y].begin() + start, map[y].begin() + end); - } - break; - - case BeatEffectColorInvert: - if (m_beatOrientation == BeatOrientRows) - { - for (int y = start; y < end; y++) - for (int x = 0; x < cols; x++) - map[y][x] ^= 0x00FFFFFF; - } - else - { - for (int y = 0; y < rows; y++) - for (int x = start; x < end; x++) - map[y][x] ^= 0x00FFFFFF; - } - break; - - case BeatEffectBlackout: - if (m_beatOrientation == BeatOrientRows) - { - for (int y = start; y < end; y++) - map[y].fill(0); - } - else - { - for (int y = 0; y < rows; y++) - for (int x = start; x < end; x++) - map[y][x] = 0; - } - break; - - case BeatEffectWhiteout: - if (m_beatOrientation == BeatOrientRows) - { - for (int y = start; y < end; y++) - map[y].fill(0x00FFFFFF); - } - else - { - for (int y = 0; y < rows; y++) - for (int x = start; x < end; x++) - map[y][x] = 0x00FFFFFF; - } - break; - - default: - break; - } } } @@ -2248,10 +1483,3 @@ bool RGBMatrixStep::checkNextStep(Function::RunOrder order, return true; } - -/************************************************************************* - * Audio Routing - *************************************************************************/ - -// Audio routing was removed: scripts pick the audio feature they listen to -// directly via script properties. RGB matrices no longer remap audio sources. diff --git a/engine/src/rgbmatrix.h b/engine/src/rgbmatrix.h index 82327b290a..6316804105 100644 --- a/engine/src/rgbmatrix.h +++ b/engine/src/rgbmatrix.h @@ -29,7 +29,6 @@ #include #include #include -#include #ifdef QT_QML_LIB #include "rgbscriptv4.h" @@ -116,7 +115,9 @@ class RGBMatrix final : public Function Color4Attr = Color1Attr + 3, Color5Attr = Color1Attr + 4, ColorLastAttr = Color1Attr + RGBAlgorithmColorDisplayCount - 1, - PatternAttr = ColorLastAttr + 1 + PatternAttr = ColorLastAttr + 1, + /** First index of the attributes dynamically registered by a Script algorithm. */ + ScriptPropertyAttr = PatternAttr + 1 }; enum { ColorAttributeCount = RGBAlgorithmColorDisplayCount }; static_assert(RGBAlgorithmColorDisplayCount >= 5, @@ -236,6 +237,29 @@ class RGBMatrix final : public Function /** Retrieve the value of the property with the given name */ QString property(QString propName); +private: + /** Return the properties of the currently loaded Script algorithm that + * are exposed as Function attributes. Index 0 of the returned list + * matches the attribute index $ScriptPropertyAttr */ + QList scriptPropertyAttributes() const; + + /** Return the attribute name used to expose the given Script property */ + static QString scriptPropertyAttributeName(const RGBScriptProperty &prop); + + /** Register a Function attribute for every property exposed by the + * currently loaded Script algorithm, so that they can be controlled + * by a VC Slider in 'Adjust' mode */ + void registerScriptPropertyAttributes(); + + /** Unregister the attributes of the currently loaded Script algorithm. + * To be called before replacing it, since the attribute names are + * retrieved from the algorithm itself */ + void unregisterScriptPropertyAttributes(); + + /** Apply the value of a Script property attribute to the algorithm. + * $attrIndex is an index of $scriptPropertyAttributes */ + void applyScriptPropertyAttribute(int attrIndex, qreal value); + private: /** A map of the custom properties for this matrix */ QMapm_properties; @@ -270,140 +294,17 @@ class RGBMatrix final : public Function /** Check what should be done when elapsed() >= duration() */ void roundCheck(); - /** Same as roundCheck but assumes m_algorithmMutex is already held. - * Returns true if the step actually advanced, false if the function - * stopped (SingleShot end) or algorithm is null. */ - bool roundCheckLocked(); - /** Check if the engine needs to be re-created */ void checkEngineCreation(); QSharedPointer getFader(Universe *universe); - void updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime, uint fadeOutTime); + void updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime); /** Update FadeChannels when $map has changed since last time */ - void updateMapChannels(const RGBMap& map, const FixtureGroup* grp, QList universes, int beatDuration); + void updateMapChannels(const RGBMap& map, const FixtureGroup* grp, QList universes); void applyColorAttribute(int colorIndex, qreal packedColor); void applyPatternAttribute(qreal patternIndex); -private: - /** Pre-computed channel writes for one fixture-head pixel. - * See rebuildPixelPlan() for invalidation triggers. */ - enum PixelValueSource : quint8 - { - VS_Red, VS_Green, VS_Blue, - VS_Cyan, VS_Magenta, VS_Yellow, - VS_White, ///< min(R,G,B) - VS_RedSubW, VS_GreenSubW, VS_BlueSubW, - VS_Grey, ///< rgbToGrey(col) - VS_GreyOrFull ///< 0 if grey == 0 else 255 - }; - - struct PixelPlanEntry - { - quint16 x; - quint16 y; - quint32 universeIndex; - quint32 fixtureID; - quint32 channel; ///< channel number relative to fixture - PixelValueSource source; - }; - - /** Flat, pre-resolved list of channel writes for the current fixture group - * + control mode. Avoids per-tick fixture/head/channel lookups. */ - QVector m_pixelPlan; - - /** Cached pointer used to detect that the resolved FixtureGroup changed - * (in addition to the dirty flag). */ - const FixtureGroup *m_pixelPlanGroup = nullptr; - - /** When non-zero, m_pixelPlan must be rebuilt before use. Marked from - * fixture / fixture-group / control-mode changes. Atomic so that signals - * from the main thread can flip it while the MasterTimer thread reads. */ - QAtomicInteger m_pixelPlanDirty; - - /** Rebuild m_pixelPlan from $grp using the current control mode and - * dimmer-control flag. Caller must hold m_algorithmMutex. */ - void rebuildPixelPlan(const FixtureGroup *grp); - - /********************************************************************* - * Phase 4: Async pre-computation of next frame's RGBMap - * - * The JS rgbMap() call dominates the MasterTimer budget. We pre-compute - * the *next* frame's pixel data on the shared JSThread at the END of the - * current write(), so that by the time the MasterTimer reaches us again - * the result is already waiting in m_precomputedMap. - * - * Validity is gated by: - * - m_precomputedReady flag (atomic): producer/consumer handshake. - * - Generation counter: invalidated on algorithm/group/control mode - * changes that would render a precomputed map unsafe or wrong. - * - Per-frame match: step index, step color, algorithm size and the - * algorithm pointer must equal what the consuming write() needs. - * - * If any check fails, we fall back to the synchronous rgbMap() path — - * correctness is preserved, only the perf win is missed. - *********************************************************************/ -private: - /** Pre-computed pixel map for the next frame. Protected by - * m_precomputedMutex. */ - RGBMap m_precomputedMap; - - /** Guards the m_precomputed* fields below. Cheap (uncontended) — the - * only writer is the JSThread async task; the only reader is the - * MasterTimer thread once per tick. */ - QMutex m_precomputedMutex; - - /** 0 = no precomputed frame available, 1 = ready to consume. */ - QAtomicInteger m_precomputedReady; - - /** Set to 1 while a JSThread async task is queued or running for this - * matrix. Used to throttle (one outstanding task at a time) and to let - * the destructor wait for completion before tearing down. */ - QAtomicInteger m_precomputedInFlight; - - /** Bumped on any change that invalidates an in-flight or stored - * precomputed map (setAlgorithm, setFixtureGroup, setControlMode, - * preRun). Color changes do NOT bump — they are validated per-frame - * via m_precomputedColor instead, so rapid color attribute fades - * don't thrash the cache. */ - QAtomicInteger m_currentGeneration; - - /** Generation snapshot taken at kick-off; compared against - * m_currentGeneration before storing/consuming the precomputed map. */ - quint32 m_precomputedGeneration; - - /** Step index, step color and algorithm size the precomputed map was - * rendered for. The consuming write() requires these to match its - * intended frame parameters. */ - int m_precomputedStep; - uint m_precomputedColor; - QSize m_precomputedAlgoSize; - - /** Algorithm pointer the precomputed map was rendered against. - * Compared on consume to detect setAlgorithm() races that the - * generation counter alone might miss. */ - RGBAlgorithm *m_precomputedAlgorithm; - - /** Kick a non-blocking pre-computation of the next frame's rgbMap on - * the JSThread. Caller must hold m_algorithmMutex. No-op if the - * algorithm isn't a Script (other algorithms are fast and don't - * benefit) or if a task is already in flight. */ - void kickAsyncRgbMap(RGBAlgorithm *algo, const QSize &algoSize, - uint stepColor, int step, quint32 generation); - - /** Delete an algorithm pointer safely. Script-typed algorithms are - * deleted via the JSThread to drain any pending precompute tasks - * (FIFO ordering) before destruction. Other types are deleted inline. */ - static void deferDeleteAlgorithm(RGBAlgorithm *algo); - -private slots: - /** Slot connected to Doc fixture/group change signals. Marks the - * PixelPlan as dirty so it will be rebuilt on the next tick. */ - void invalidatePixelPlan(quint32 id = 0); - -private: - public: /** Convert color values to fader value */ static uchar rgbToGrey(uint col); @@ -451,9 +352,7 @@ private slots: ControlModeAmber, ControlModeUV, ControlModeDimmer, - ControlModeShutter, - ControlModeRgbw = 6, - ControlModeRgbwBrighter = 7 + ControlModeShutter }; /** Get/Set the control mode associated to this RGBMatrix */ @@ -468,117 +367,6 @@ private slots: private: RGBMatrix::ControlMode m_controlMode; - - /************************************************************************* - * Rotation & Mirror - *************************************************************************/ -public: - /** Mirror blend algorithms */ - enum MirrorBlend - { - MirrorFlip = 0, ///< Pure reflection (copy one half to other) - MirrorMax, ///< qMax per channel (LedFX-style bloom) - MirrorAverage, ///< (a + b) / 2 per channel - MirrorAdditive ///< min(255, a + b) per channel - }; - - /** Get/Set rotation: 0=0°, 1=90°CW, 2=180°, 3=270°CW */ - int rotation() const; - void setRotation(int r); - - /** Get/Set mirror: 0=off, 1=horizontal, 2=vertical, 3=both */ - int mirror() const; - void setMirror(int m); - - /** Get/Set mirror blend algorithm */ - MirrorBlend mirrorBlend() const; - void setMirrorBlend(MirrorBlend b); - - /** Return the effective algorithm size, accounting for 90/270 rotation */ - QSize effectiveAlgorithmSize() const; - - static QString mirrorBlendToString(MirrorBlend b); - static MirrorBlend stringToMirrorBlend(const QString &s); - - /** Apply rotation and mirror transforms to a rendered RGB map. - * srcSize = the size the algorithm was given (may be swapped for 90/270). - * dstSize = the physical fixture group size. */ - static void applyTransforms(RGBMap &map, const QSize &srcSize, const QSize &dstSize, - int rotation, int mirror, MirrorBlend blend); - -private: - /** Return effective algorithm size for a given fixture group */ - QSize effectiveAlgorithmSize(const FixtureGroup *grp) const; - - int m_rotation; - int m_mirror; - MirrorBlend m_mirrorBlend; - - /************************************************************************* - * Brightness multiplier - *************************************************************************/ -public: - /** Get/Set the post-render brightness multiplier (>= 0.0, 1.0 = unity) */ - qreal brightness() const; - void setBrightness(qreal b); - -private: - qreal m_brightness; - - /************************************************************************* - * Beat Transform - *************************************************************************/ -public: - enum BeatEffect - { - BeatEffectOff = 0, - BeatEffectMirror, - BeatEffectColorInvert, - BeatEffectBlackout, - BeatEffectWhiteout - }; - - enum BeatSelection - { - BeatSelAllOnDownbeat = 0, - BeatSelWalk, - BeatSelRandom - }; - - enum BeatOrientation - { - BeatOrientRows = 0, - BeatOrientColumns - }; - - BeatEffect beatEffect() const; - void setBeatEffect(BeatEffect e); - - BeatSelection beatSelection() const; - void setBeatSelection(BeatSelection s); - - BeatOrientation beatOrientation() const; - void setBeatOrientation(BeatOrientation o); - - static QString beatEffectToString(BeatEffect e); - static BeatEffect stringToBeatEffect(const QString &s); - static QString beatSelectionToString(BeatSelection s); - static BeatSelection stringToBeatSelection(const QString &s); - static QString beatOrientationToString(BeatOrientation o); - static BeatOrientation stringToBeatOrientation(const QString &s); - -private: - void applyBeatTransform(RGBMap &map, int currentBeat, int beatsPerBar); - - static void segmentRange(int segment, int total, int segmentsCount, int &start, int &end); - - BeatEffect m_beatEffect; - BeatSelection m_beatSelection; - BeatOrientation m_beatOrientation; - int m_currentBeat; - int m_lastBeat; - int m_randomSegment; - }; /** @} */ diff --git a/engine/src/rgbscriptv4.cpp b/engine/src/rgbscriptv4.cpp index bce999a30e..7ab0374c25 100644 --- a/engine/src/rgbscriptv4.cpp +++ b/engine/src/rgbscriptv4.cpp @@ -19,135 +19,21 @@ #include #include -#include -#include #include -#include #include #include #include -#include // cppcheck-suppress missingIncludeSystem #include // cppcheck-suppress missingIncludeSystem #include -#include -#include -#include #include "rgbscriptv4.h" #include "rgbscriptscache.h" -#include "audiochannel.h" -#include "audiocapture.h" -#include "audioprofile.h" -#include "audiosnapshot.h" -#include "mastertimer.h" #include "qlcconfig.h" #include "qlcfile.h" -#include "doc.h" -#include "rgbmatrix.h" - -namespace -{ - RGBMatrix *owningMatrix(Doc *doc, const RGBScript *script) - { - if (doc == NULL || script == NULL) - return NULL; - - foreach (Function *function, doc->functionsByType(Function::RGBMatrixType)) - { - RGBMatrix *matrix = qobject_cast (function); - if (matrix != NULL && matrix->algorithm() == script) - return matrix; - } - - return NULL; - } - - static constexpr int kAudioApiVersion = 5; - static constexpr int kBeatsPerBar = 4; - static constexpr double kPi = 3.14159265358979323846; - - /** Convert HSV (h,s,v each in [0,1]) to packed 0xAARRGGBB via qRgb(). */ - static inline uint hsvToRgb(float h, float s, float v) - { - if (!std::isfinite(h)) h = 0.0f; - if (!std::isfinite(s)) s = 0.0f; - if (!std::isfinite(v)) v = 0.0f; - h = h - floorf(h); if (h < 0) h += 1.0f; - s = qBound(0.0f, s, 1.0f); - v = qBound(0.0f, v, 1.0f); - - float c = v * s; - float x = c * (1.0f - fabsf(fmodf(h * 6.0f, 2.0f) - 1.0f)); - float m = v - c; - float r, g, b; - int sector = (int)(h * 6.0f) % 6; - switch (sector) { - case 0: r=c; g=x; b=0; break; - case 1: r=x; g=c; b=0; break; - case 2: r=0; g=c; b=x; break; - case 3: r=0; g=x; b=c; break; - case 4: r=x; g=0; b=c; break; - default: r=c; g=0; b=x; break; - } - int ri = qRound((r+m) * 255.0f); - int gi = qRound((g+m) * 255.0f); - int bi = qRound((b+m) * 255.0f); - return qRgb(ri, gi, bi); - } - - /** Convert packed 0xRRGGBB to HSV floats in [0,1]. */ - struct HsvColor { float h, s, v; }; - - /** Marshal an HsvColor into a QJSValue {h,s,v} object. */ - static inline QJSValue hsvToJs(QJSEngine *engine, const HsvColor &hsv) - { - QJSValue obj = engine->newObject(); - obj.setProperty(QStringLiteral("h"), QJSValue(double(hsv.h))); - obj.setProperty(QStringLiteral("s"), QJSValue(double(hsv.s))); - obj.setProperty(QStringLiteral("v"), QJSValue(double(hsv.v))); - return obj; - } - - static inline HsvColor rgbToHsv(uint packed) - { - float r = float((packed >> 16) & 0xFF) / 255.0f; - float g = float((packed >> 8) & 0xFF) / 255.0f; - float b = float(packed & 0xFF) / 255.0f; - float mx = std::max({r, g, b}); - float mn = std::min({r, g, b}); - float d = mx - mn; - float h = 0.0f; - float s = (mx == 0.0f) ? 0.0f : d / mx; - if (d != 0.0f) { - if (mx == r) h = fmodf((g - b) / d + 6.0f, 6.0f) / 6.0f; - else if (mx == g) h = ((b - r) / d + 2.0f) / 6.0f; - else h = ((r - g) / d + 4.0f) / 6.0f; - } - return {h, s, mx}; - } - - bool onsetFiredAt(const AudioSnapshot &snap, int methodIndex) - { - switch (methodIndex) - { - case 0: return snap.onsets.energy; - case 1: return snap.onsets.hfc; - case 2: return snap.onsets.complex_; - case 3: return snap.onsets.phase; - case 4: return snap.onsets.wphase; - case 5: return snap.onsets.specdiff; - case 6: return snap.onsets.kl; - case 7: return snap.onsets.mkl; - case 8: return snap.onsets.specflux; - default: return false; - } - } - -} /**************************************************************************** * Initialization @@ -173,10 +59,6 @@ class JSThread final : public QThread RGBScript::RGBScript(Doc *doc) : RGBAlgorithm(doc) , m_apiVersion(0) - , m_usesAudio(false) - , m_audioInput(NULL) - , m_audioRegistered(false) - , m_hsvContractValidated(false) { } @@ -185,10 +67,6 @@ RGBScript::RGBScript(const RGBScript& s) , m_fileName(s.m_fileName) , m_contents(s.m_contents) , m_apiVersion(0) - , m_usesAudio(false) - , m_audioInput(NULL) - , m_audioRegistered(false) - , m_hsvContractValidated(false) { evaluate(); foreach (RGBScriptProperty cap, s.m_properties) @@ -199,7 +77,6 @@ RGBScript::RGBScript(const RGBScript& s) RGBScript::~RGBScript() { - teardownAudioCapture(); } RGBScript &RGBScript::operator=(const RGBScript &s) @@ -277,29 +154,6 @@ void RGBScript::initEngine() // cppcheck-suppress unknownMacro qAddPostRoutine(RGBScript::cleanupEngine); s_jsThread->ready.acquire(1); - - // Load shared audio script helpers into the engine's global scope. - QDir scriptsDir = RGBScriptsCache::systemScriptsDirectory(); - const QStringList shimNames = { - QStringLiteral("hsvutil.js") - }; - for (const QString &shimName : shimNames) - { - QString shimPath = scriptsDir.filePath(shimName); - QFile shimFile(shimPath); - if (shimFile.open(QIODevice::ReadOnly)) - { - QString shimContents = QTextStream(&shimFile).readAll(); - shimFile.close(); - QMetaObject::invokeMethod(s_jsThread->engine, [shimContents, shimPath]{ - QJSValue result = s_jsThread->engine->evaluate(shimContents, shimPath); - if (result.isError()) - displayError(result, shimPath); - else - qDebug() << "[RGBScript] Loaded RGB script shim" << shimPath; - }, Qt::BlockingQueuedConnection); - } - } } Q_ASSERT(s_jsThread->engine != NULL); } @@ -312,15 +166,6 @@ void RGBScript::cleanupEngine() s_jsThread = NULL; } -bool RGBScript::scheduleOnJSThread(std::function fn) -{ - if (s_jsThread == NULL || s_jsThread->engine == NULL) - return false; - - QMetaObject::invokeMethod(s_jsThread->engine, std::move(fn), Qt::QueuedConnection); - return true; -} - bool RGBScript::evaluate() { @@ -335,8 +180,6 @@ bool RGBScript::evaluate() m_rgbMapStepCount = QJSValue(); m_rgbMapSetColors = QJSValue(); m_apiVersion = 0; - m_usesAudio = false; - m_hsvContractValidated = false; if (m_fileName.isEmpty() || m_contents.isEmpty()) { @@ -370,45 +213,6 @@ bool RGBScript::evaluate() m_apiVersion = m_script.property("apiVersion").toInt(); if (m_apiVersion > 0) { - // Check if the script requests audio data - QJSValue usesAudioVal = m_script.property("usesAudio"); - m_usesAudio = (!usesAudioVal.isUndefined() && usesAudioVal.toBool()); - - // Extract audio input categories: prefer explicit algo.audioInputs, - // fall back to auto-detection from source. - m_audioInputCategories.clear(); - if (m_usesAudio) - { - QJSValue inputsVal = m_script.property(QStringLiteral("audioInputs")); - if (inputsVal.isArray()) - { - const int len = inputsVal.property(QStringLiteral("length")).toInt(); - for (int i = 0; i < len; ++i) - { - QString cat = inputsVal.property(quint32(i)).toString(); - if (!cat.isEmpty() && !m_audioInputCategories.contains(cat)) - m_audioInputCategories.append(cat); - } - } - else - { - // Auto-detect from source: extract top-level audio.X references - static const QRegularExpression rx(QStringLiteral("\\baudio\\.(\\w+)")); - QRegularExpressionMatchIterator it = rx.globalMatch(m_contents); - QSet seen; - while (it.hasNext()) - { - QString cat = it.next().captured(1); - if (cat != QStringLiteral("timing") && !seen.contains(cat)) - { - seen.insert(cat); - m_audioInputCategories.append(cat); - } - } - m_audioInputCategories.sort(); - } - } - if (m_apiVersion >= 3) { m_rgbMapSetColors = m_script.property(QStringLiteral("rgbMapSetColors")); @@ -492,15 +296,9 @@ void RGBScript::rgbMapSetColors(const QVector &colors) int accColors = acceptColors(); int rawColorCount = colors.count(); - // HSV-only contract: scripts receive an array of {h,s,v} objects, never - // packed RGB integers. Convert each stop here. - QJSEngine *engine = s_jsThread->engine; - QJSValue jsRawColors = engine->newArray(accColors); + QJSValue jsRawColors = s_jsThread->engine->newArray(accColors); for (int i = 0; i < rawColorCount && i < accColors; i++) - { - HsvColor hsv = rgbToHsv(colors.at(i) & 0xFFFFFFu); - jsRawColors.setProperty(i, hsvToJs(engine, hsv)); - } + jsRawColors.setProperty(i, QJSValue(colors.at(i))); QJSValueList args; args << jsRawColors; @@ -546,118 +344,34 @@ void RGBScript::rgbMap(const QSize& size, uint rgb, int step, RGBMap &map) if (m_rgbMap.isUndefined() == true) return; - // Resolve owning matrix once per frame — avoids repeated O(N) linear scan. - RGBMatrix *matrix = owningMatrix(doc(), this); - - // Inject the user's color palette as algo.colors (array of {h,s,v}). - injectColors(rgb, matrix); - - // If this is an audio-aware script, set up audio capture on first call - // and inject audio data as a 5th argument - if (m_usesAudio) - { - setupAudioCapture(); - } - + // Call the rgbMap function QJSValueList args; - // 3rd argument is the primary color as {h,s,v} (HSV-only contract). - // Same value as algo.color set by injectGradientArrays(); kept positional - // so the rgbMap(width, height, color, step[, audio]) signature is stable. - QJSEngine *engine = s_jsThread->engine; - HsvColor primary = rgbToHsv(rgb & 0xFFFFFFu); - args << size.width() << size.height() << hsvToJs(engine, primary) << step; - - if (m_usesAudio) - args << buildAudioDataObject(); + args << size.width() << size.height() << rgb << step; QJSValue yarray(m_rgbMap.call(args)); if (yarray.isError()) - { displayError(yarray, m_fileName); - return; - } - - // HSV-only contract: scripts MUST return a Float32Array of length - // width*height*3 (interleaved H,S,V floats in [0,1]). The engine converts - // to packed RGB here. Returning a Uint32Array (the legacy RGB path) is - // no longer supported. - const int width = size.width(); - const int height = size.height(); - if (width <= 0 || height <= 0) - return; - - const int expectedHsvBytes = width * height * 3 * 4; // Float32Array: 3 floats/pixel - // Full validation on first frame; fast-path on subsequent frames. - if (!m_hsvContractValidated) + // Check the matrix to be a valid matrix + if (yarray.isArray()) { - QJSValue bpeProp = yarray.property(QStringLiteral("BYTES_PER_ELEMENT")); - if (!bpeProp.isNumber()) - { - qWarning() << "RGBScript" << m_fileName - << "rgbMap() did not return a TypedArray. HSV contract" - << "requires a flat Float32Array of length width*height*3."; - return; - } + QVariantList yvArray = yarray.toVariant().toList(); + int ylen = yvArray.length(); + map.resize(ylen); - QJSValue ctorName = yarray.property(QStringLiteral("constructor")) - .property(QStringLiteral("name")); - if (ctorName.toString() != QStringLiteral("Float32Array")) + for (int y = 0; y < ylen && y < size.height(); y++) { - qWarning() << "RGBScript" << m_fileName - << "returned" << ctorName.toString() - << "but only Float32Array (HSV) is supported."; - return; - } + QVariantList xvArray = yvArray.at(y).toList(); + int xlen = xvArray.length(); + map[y].resize(xlen); - if (bpeProp.toInt() != 4) - { - qWarning() << "RGBScript" << m_fileName - << "unexpected BYTES_PER_ELEMENT:" << bpeProp.toInt(); - return; + for (int x = 0; x < xlen && x < size.width(); x++) + map[y][x] = xvArray.at(x).toUInt(); } - - m_hsvContractValidated = true; } - - const int byteLength = yarray.property(QStringLiteral("byteLength")).toInt(); - if (byteLength != expectedHsvBytes) - { - qWarning() << "RGBScript" << m_fileName - << "TypedArray size mismatch: byteLength=" << byteLength - << "(expected" << expectedHsvBytes << ")"; - return; - } - - QJSValue bufProp = yarray.property(QStringLiteral("buffer")); - if (!bufProp.isObject()) - { - qWarning() << "RGBScript" << m_fileName - << "TypedArray has no underlying ArrayBuffer"; - return; - } - - QByteArray bytes = bufProp.toVariant().toByteArray(); - if (bytes.size() != expectedHsvBytes) - { - qWarning() << "RGBScript" << m_fileName - << "ArrayBuffer extraction size mismatch: got" << bytes.size() - << "expected" << expectedHsvBytes; - return; - } - - map.resize(height); - // Convert interleaved float H,S,V triples to packed RGB. - const float *src = reinterpret_cast(bytes.constData()); - for (int y = 0; y < height; ++y) + else { - map[y].resize(width); - const float *row = src + (y * width * 3); - for (int x = 0; x < width; ++x) - { - int i = x * 3; - map[y][x] = hsvToRgb(row[i], row[i + 1], row[i + 2]); - } + qWarning() << "Returned value is not an array within an array!"; } } @@ -741,164 +455,6 @@ bool RGBScript::saveXML(QXmlStreamWriter *doc) const } } -bool RGBScript::usesAudio() const -{ - return m_usesAudio; -} - -QStringList RGBScript::audioInputCategories() const -{ - return m_audioInputCategories; -} - -void RGBScript::setDisplaySize(const QSize &size) -{ - if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) - { - QMetaObject::invokeMethod(s_jsThread->engine, [this, size]{ setDisplaySize(size); }, Qt::BlockingQueuedConnection); - return; - } - - if (!m_script.isObject()) - return; - - m_script.setProperty(QStringLiteral("displayWidth"), size.width()); - m_script.setProperty(QStringLiteral("displayHeight"), size.height()); -} - -void RGBScript::postRun() -{ - teardownAudioCapture(); -} - -/**************************************************************************** - * Audio support - ****************************************************************************/ - -void RGBScript::setupAudioCapture() -{ - if (doc() == NULL) - return; - - QSharedPointer capture = doc()->audioInputCapture(); - if (capture.isNull()) - return; - - if (m_audioInput != NULL && capture.data() == m_audioInput) - return; // Already connected to this capture - - teardownAudioCapture(); - m_audioInput = capture.data(); - - // The audio object is sourced exclusively from the AudioProfile's - // AudioChannel snapshot inside buildAudioDataObject(). We don't need - // to subscribe to aubioDataReady — we just need the capture thread - // to be running. - m_audioInput->registerBandsNumber(1); - m_audioRegistered = true; -} - -void RGBScript::teardownAudioCapture() -{ - if (m_audioInput != NULL) - { - if (m_audioRegistered) - { - m_audioInput->unregisterBandsNumber(1); - m_audioRegistered = false; - } - m_audioInput = NULL; - } -} - -void RGBScript::injectColors(uint rgb, RGBMatrix *matrix) -{ - QJSEngine *engine = s_jsThread->engine; - if (engine == NULL || m_script.isUndefined()) - return; - - int numColors = acceptColors(); - if (numColors <= 0) - { - m_script.setProperty(QStringLiteral("colors"), engine->newArray(0)); - return; - } - - // Convert user-picked color stops to HSV. Invalid/missing slots get - // the step color fallback so algo.colors always has exactly numColors elements. - HsvColor fallback = rgbToHsv(rgb & 0xFFFFFFu); - QVector cols; - if (matrix != NULL) - cols = matrix->getColors(); - - // Check if any user-picked color is set (vs all being fallback/default). - bool anyUserSet = false; - QJSValue colorsArr = engine->newArray(quint32(numColors)); - for (int i = 0; i < numColors; ++i) - { - bool valid = (i < cols.size() && cols.at(i).isValid()); - if (valid) anyUserSet = true; - HsvColor hsv = valid ? rgbToHsv(cols.at(i).rgb() & 0xFFFFFFu) : fallback; - colorsArr.setProperty(quint32(i), hsvToJs(engine, hsv)); - } - - m_script.setProperty(QStringLiteral("colors"), colorsArr); - m_script.setProperty(QStringLiteral("hasUserColors"), QJSValue(anyUserSet)); -} - -QJSValue RGBScript::buildAudioDataObject() -{ - QJSEngine *engine = s_jsThread->engine; - QJSValue audioObj = engine->newObject(); - AudioChannel *channel = NULL; - AudioChannelConfig config = AudioChannelConfig::defaults(); - Doc *currentDoc = doc(); - AudioProfile *profile = NULL; - if (currentDoc != NULL) - { - profile = currentDoc->audioProfile(currentDoc->activeAudioProfileId()); - if (profile == NULL) - profile = currentDoc->defaultAudioProfile(); - } - if (profile != NULL) - { - channel = profile->channel(); - config = (channel != NULL) ? channel->config() : profile->channelConfig(); - } - - AudioSnapshot snap; - if (channel != NULL) - snap = channel->snapshot(); - - const int onsetMethodIndex = std::clamp(config.aubio.onsetMethodIndex, - 0, AUBIO_ONSET_METHODS - 1); - - const bool tempoActive = snap.music.bpm > 0.0; - const double bpm = tempoActive ? snap.music.bpm : 120.0; - const double beatPhase = snap.music.beatPhase; - const double dt = (double(MasterTimer::tick()) / 1000.0) * (bpm / 60.0); - - audioObj.setProperty(QStringLiteral("beat"), QJSValue(snap.beatPower)); - audioObj.setProperty(QStringLiteral("bass"), QJSValue(snap.bassPower)); - audioObj.setProperty(QStringLiteral("low"), QJSValue(snap.lows)); - audioObj.setProperty(QStringLiteral("mid"), QJSValue(snap.mids)); - audioObj.setProperty(QStringLiteral("high"), QJSValue(snap.highs)); - audioObj.setProperty(QStringLiteral("onset"), QJSValue(onsetFiredAt(snap, onsetMethodIndex))); - audioObj.setProperty(QStringLiteral("onsetIntensity"), QJSValue(snap.onsets.thresholdedDescriptors[onsetMethodIndex])); - audioObj.setProperty(QStringLiteral("beatFired"), QJSValue(snap.beatTrigger.firedThisFrame)); - audioObj.setProperty(QStringLiteral("downbeat"), QJSValue(snap.downbeatFired)); - audioObj.setProperty(QStringLiteral("bpm"), QJSValue(bpm)); - audioObj.setProperty(QStringLiteral("phase"), QJSValue(beatPhase)); - audioObj.setProperty(QStringLiteral("barPhase"), QJSValue(snap.music.barPhase / double(kBeatsPerBar))); - audioObj.setProperty(QStringLiteral("dt"), QJSValue(tempoActive ? dt : 0.0)); - audioObj.setProperty(QStringLiteral("cosPulse"), QJSValue(tempoActive - ? std::max(0.0, std::cos(beatPhase * kPi)) : 0.0)); - audioObj.setProperty(QStringLiteral("version"), QJSValue(kAudioApiVersion)); - - return audioObj; -} - - /************************************************************************ * Capabilities ************************************************************************/ diff --git a/engine/src/rgbscriptv4.h b/engine/src/rgbscriptv4.h index ebfbb7e92f..d6da2d9b3f 100644 --- a/engine/src/rgbscriptv4.h +++ b/engine/src/rgbscriptv4.h @@ -22,14 +22,10 @@ #include #include -#include -#include #include "rgbalgorithm.h" #include "rgbscriptproperty.h" -class AudioCapture; -class RGBMatrix; class QJSEngine; class QDir; @@ -72,16 +68,6 @@ class RGBScript final : public RGBAlgorithm /** Evaluate the script's contents and see if it checks out */ bool evaluate(); - /** Phase 4: Schedule a callable to run on the shared JS thread. - * Used to dispatch async rgbMap pre-computations without blocking the - * MasterTimer thread, and to safely defer deletion of script-typed - * algorithms after any in-flight tasks have drained (FIFO). - * - * Returns false if the JS thread is not running (caller should fall - * back to a synchronous path). Safe to call from any thread. - */ - static bool scheduleOnJSThread(std::function fn); - private: static void initEngine(); static void cleanupEngine(); @@ -125,18 +111,6 @@ class RGBScript final : public RGBAlgorithm /** @reimp */ int acceptColors() const override; - /** @reimp */ - bool usesAudio() const override; - - /** @reimp */ - QStringList audioInputCategories() const override; - - /** @reimp */ - void setDisplaySize(const QSize &size) override; - - /** @reimp */ - void postRun() override; - /** @reimp */ bool loadXML(QXmlStreamReader &root) override; @@ -150,38 +124,6 @@ class RGBScript final : public RGBAlgorithm QJSValue m_rgbMapStepCount; //! rgbMapStepCount() function QJSValue m_rgbMapSetColors; //! rgbMapSetColors() function QJSValue m_rgbMapGetColors; //! rgbMapSetColors() function - bool m_usesAudio; //! Whether the script declared algo.usesAudio = true - QStringList m_audioInputCategories; //! Top-level audio.X categories used by the script - - /************************************************************************ - * Audio support - ************************************************************************/ -private: - /** Connect to AudioCapture and register for spectrum data */ - void setupAudioCapture(); - - /** Disconnect from AudioCapture and release resources */ - void teardownAudioCapture(); - - /** Build a JS object with current audio data to pass as 5th arg to rgbMap */ - QJSValue buildAudioDataObject(); - - /** - * Build the gradientColors / gradientBandColors HSV arrays from the owning - * RGBMatrix and inject them as properties on m_script (the algo object). - * Also injects algo.color (primary HSV color) and stashes the arrays on - * m_currentGradientColors / m_currentBandColors so buildAudioDataObject() - * can republish them on audio.colors. - * - * @param rgb fallback color (packed 0xRRGGBB) used when the matrix has - * no valid color stops; converted to HSV before injection. - */ - void injectColors(uint rgb, RGBMatrix *matrix); - -private: - AudioCapture *m_audioInput; - bool m_audioRegistered; - bool m_hsvContractValidated; //! Skip Float32Array type checks after first frame /************************************************************************ * Properties diff --git a/engine/test/rgbmatrix/rgbmatrix_test.cpp b/engine/test/rgbmatrix/rgbmatrix_test.cpp index 72fca7369c..e1c74362c9 100644 --- a/engine/test/rgbmatrix/rgbmatrix_test.cpp +++ b/engine/test/rgbmatrix/rgbmatrix_test.cpp @@ -82,7 +82,7 @@ void RGBMatrix_Test::initial() RGBMatrix mtx(m_doc); QCOMPARE(mtx.type(), Function::RGBMatrixType); QCOMPARE(mtx.fixtureGroup(), FixtureGroup::invalidId()); - QCOMPARE(mtx.getColor(0), QColor()); + QCOMPARE(mtx.getColor(0), QColor(Qt::red)); QCOMPARE(mtx.getColor(1), QColor()); QCOMPARE(mtx.m_fadersMap.count(), 0); QCOMPARE(mtx.m_stepHandler->currentStepIndex(), 0); @@ -164,7 +164,6 @@ void RGBMatrix_Test::previewMaps() mtx.setTotalDuration(8000); QCOMPARE(mtx.totalDuration(), uint(8000)); - handler.setStepColor(QColor(Qt::red)); mtx.previewMap(0, &handler); QCOMPARE(handler.m_map.size(), 5); @@ -175,15 +174,13 @@ void RGBMatrix_Test::previewMaps() { for (int x = 0; x < 5; x++) { - const QRgb expected = x == z ? QColor(Qt::red).rgb() - : QColor(Qt::black).rgb(); - QCOMPARE(handler.m_map[y][x], expected); + if (x == z) + QCOMPARE(handler.m_map[y][x], QColor(Qt::black).rgb()); + else + QCOMPARE(handler.m_map[y][x], uint(0)); } } } - qInfo() << "Preview pattern final step:" - << handler.m_map[0][4] << "stripe," - << handler.m_map[0][0] << "background"; } void RGBMatrix_Test::property() diff --git a/engine/test/rgbscript/CMakeLists.txt b/engine/test/rgbscript/CMakeLists.txt index b96069c7ef..6aec6caf7c 100644 --- a/engine/test/rgbscript/CMakeLists.txt +++ b/engine/test/rgbscript/CMakeLists.txt @@ -14,20 +14,6 @@ target_link_libraries(rgbscript_test PRIVATE Qt${QT_MAJOR_VERSION}::Test qlcplusengine ) -target_compile_definitions(rgbscript_test PRIVATE - RGBSCRIPTS_SOURCE_DIR="${CMAKE_SOURCE_DIR}/resources/rgbscripts") - -if(APPLE) - set(RGBSCRIPT_TEST_RESOURCE_DIR - "${CMAKE_CURRENT_BINARY_DIR}/../Resources/RGBScripts") - file(MAKE_DIRECTORY "${RGBSCRIPT_TEST_RESOURCE_DIR}") - if(NOT EXISTS "${RGBSCRIPT_TEST_RESOURCE_DIR}/hsvutil.js") - file(CREATE_LINK - "${CMAKE_SOURCE_DIR}/resources/rgbscripts/hsvutil.js" - "${RGBSCRIPT_TEST_RESOURCE_DIR}/hsvutil.js" - SYMBOLIC COPY_ON_ERROR) - endif() -endif() if(qmlui OR (QT_VERSION_MAJOR GREATER 5)) target_link_libraries(rgbscript_test PRIVATE diff --git a/engine/test/rgbscript/rgbscript_test.cpp b/engine/test/rgbscript/rgbscript_test.cpp index 3828c38e6a..dec54404a4 100644 --- a/engine/test/rgbscript/rgbscript_test.cpp +++ b/engine/test/rgbscript/rgbscript_test.cpp @@ -31,22 +31,9 @@ #undef private #include "doc.h" -#include "rgbmatrix.h" #include "../common/resource_paths.h" -namespace { - -QString capturedWarning; - -void captureWarning(QtMsgType type, const QMessageLogContext &, const QString &message) -{ - if (type == QtWarningMsg) - capturedWarning = message; -} - -} - void RGBScript_Test::initTestCase() { m_doc = new Doc(this); @@ -79,7 +66,6 @@ void RGBScript_Test::directories() QString path("%1/../%2"); QCOMPARE(dir.path(), path.arg(QCoreApplication::applicationDirPath()) .arg("Resources/RGBScripts")); - QVERIFY(QFileInfo::exists(dir.filePath("hsvutil.js"))); #elif defined(WIN32) || defined(Q_OS_WIN) QVERIFY(dir.path().endsWith("RGBScripts")); #else @@ -106,27 +92,33 @@ void RGBScript_Test::scripts() QVERIFY(dir.entryList().size() > 0); // Prepare check that file is registered for delivery - QString proFilePath = QDir(RGBSCRIPTS_SOURCE_DIR).filePath("CMakeLists.txt"); + QString proFilePath = dir.filePath("CMakeLists.txt"); QFile proFile(proFilePath); - QVERIFY(proFile.open(QIODevice::ReadOnly)); - const QString cmakeContents = QTextStream(&proFile).readAll(); + QVERIFY(proFile.open(QIODevice::ReadWrite)); + QTextStream pro (&proFile); // Catch syntax / JS engine errors explicitly in the test. foreach (QString file, dir.entryList()) { + RGBScript* script = new RGBScript(m_doc); + QFile absFile(dir.absoluteFilePath(file)); + QVERIFY(script->load(absFile.fileName())); + qDebug() << "Searching '" + file + "' in CMakeLists.txt"; // Check that the script is listed in the cmake file. if (file != "empty.js") { QString searchString = " " + file; - QVERIFY(cmakeContents.contains(searchString, Qt::CaseSensitive)); - } - - if (file == "hsvutil.js") - continue; + QString line; + bool foundInProFile = false; + do { + line = pro.readLine(); + if (line.contains(searchString, Qt::CaseSensitive)) { + foundInProFile = true; + } + } while (!line.isNull() && foundInProFile == false); - RGBScript script(m_doc); - QFile absFile(dir.absoluteFilePath(file)); - QVERIFY(script.load(absFile.fileName())); + QVERIFY(foundInProFile); + } } proFile.close(); @@ -169,16 +161,6 @@ void RGBScript_Test::script() QVERIFY(s->m_rgbMapStepCount.isValid() == true); #endif delete s; - - s = m_doc->rgbScriptsCache()->script("Audio Fire"); - const QString originalSpeed = s->property("speed"); - const QString changedSpeed = QStringLiteral("0.08"); - QVERIFY(originalSpeed != changedSpeed); - QVERIFY(s->setProperty("speed", changedSpeed)); - QCOMPARE(s->property("speed"), changedSpeed); - qInfo().noquote() << "Float property speed:" << originalSpeed - << "->" << s->property("speed"); - delete s; } void RGBScript_Test::evaluateException() @@ -186,17 +168,8 @@ void RGBScript_Test::evaluateException() // Should be function() QString code("( function { return 5; } )()"); RGBScript s(m_doc); - s.m_fileName = QStringLiteral("malformed-syntax.js"); s.m_contents = code; - capturedWarning.clear(); - QtMessageHandler previousHandler = qInstallMessageHandler(captureWarning); - const bool result = s.evaluate(); - qInstallMessageHandler(previousHandler); - QCOMPARE(result, false); - QVERIFY2(capturedWarning.contains("malformed-syntax.js: Exception at line") && - capturedWarning.contains("SyntaxError"), - qPrintable(capturedWarning)); - qInfo().noquote() << "Captured diagnostic:" << capturedWarning; + QCOMPARE(s.evaluate(), false); } void RGBScript_Test::evaluateNoRgbMapFunction() @@ -205,17 +178,8 @@ void RGBScript_Test::evaluateNoRgbMapFunction() QString code("( function() { return 5; } )()"); RGBScript s(m_doc); RGBMap map; - s.m_fileName = QStringLiteral("missing-rgb-map.js"); s.m_contents = code; - capturedWarning.clear(); - QtMessageHandler previousHandler = qInstallMessageHandler(captureWarning); - const bool result = s.evaluate(); - qInstallMessageHandler(previousHandler); - QCOMPARE(result, false); - QVERIFY2(capturedWarning.contains("missing-rgb-map.js") && - capturedWarning.contains("missing the rgbMap() function"), - qPrintable(capturedWarning)); - qInfo().noquote() << "Captured diagnostic:" << capturedWarning; + QCOMPARE(s.evaluate(), false); s.rgbMap(QSize(5, 5), 1, 0, map); QCOMPARE(map, RGBMap()); } @@ -225,17 +189,8 @@ void RGBScript_Test::evaluateNoRgbMapStepCountFunction() // No rgbMapStepCount() function present QString code("( function() { var foo = new Object; foo.rgbMap = function() { return 0; }; return foo; } )()"); RGBScript s(m_doc); - s.m_fileName = QStringLiteral("missing-step-count.js"); s.m_contents = code; - capturedWarning.clear(); - QtMessageHandler previousHandler = qInstallMessageHandler(captureWarning); - const bool result = s.evaluate(); - qInstallMessageHandler(previousHandler); - QCOMPARE(result, false); - QVERIFY2(capturedWarning.contains("missing-step-count.js") && - capturedWarning.contains("missing the rgbMapStepCount() function"), - qPrintable(capturedWarning)); - qInfo().noquote() << "Captured diagnostic:" << capturedWarning; + QCOMPARE(s.evaluate(), false); QCOMPARE(s.rgbMapStepCount(QSize(5, 5)), -1); } @@ -244,17 +199,8 @@ void RGBScript_Test::evaluateInvalidApiVersion() // No apiVersion property QString code("( function() { var foo = new Object; foo.rgbMap = function() { return 0; }; foo.rgbMapStepCount = function(width, height) { return 0; }; return foo; } )()"); RGBScript s(m_doc); - s.m_fileName = QStringLiteral("invalid-api-version.js"); s.m_contents = code; - capturedWarning.clear(); - QtMessageHandler previousHandler = qInstallMessageHandler(captureWarning); - const bool result = s.evaluate(); - qInstallMessageHandler(previousHandler); - QCOMPARE(result, false); - QVERIFY2(capturedWarning.contains("invalid-api-version.js") && - capturedWarning.contains("has an invalid apiVersion: 0"), - qPrintable(capturedWarning)); - qInfo().noquote() << "Captured diagnostic:" << capturedWarning; + QCOMPARE(s.evaluate(), false); } void RGBScript_Test::rgbMapStepCount() @@ -269,17 +215,13 @@ void RGBScript_Test::rgbMapColorArray() RGBMap map; RGBScript* s = m_doc->rgbScriptsCache()->script("Alternate"); QCOMPARE(s->evaluate(), true); - RGBMatrix* matrix = new RGBMatrix(m_doc); - matrix->setAlgorithm(s); - matrix->setColor(0, QColor(Qt::red)); - matrix->setColor(1, QColor(Qt::green)); - QVERIFY(m_doc->addFunction(matrix)); QVector rawRgbColors = { - QColor(Qt::red).rgb(), - QColor(Qt::green).rgb() + QColor(Qt::red).rgb() & 0x00ffffff, + QColor(Qt::green).rgb() & 0x00ffffff }; QSize mapSize = QSize(5, 5); + s->rgbMapSetColors(rawRgbColors); s->rgbMap(mapSize, 0, 0, map); QVERIFY(map.isEmpty() == false); @@ -295,7 +237,7 @@ void RGBScript_Test::rgbMapColorArray() QCOMPARE(map[y][x], rawRgbColors[0]); } } - QVERIFY(m_doc->deleteFunction(matrix->id())); + delete s; } void RGBScript_Test::rgbMap() @@ -304,7 +246,7 @@ void RGBScript_Test::rgbMap() RGBScript* s = m_doc->rgbScriptsCache()->script("Stripes"); QVector rawRgbColors = { QColor(Qt::red).rgb(), - QColor(Qt::black).rgb() + uint(0) }; s->rgbMap(QSize(3, 4), 0, 0, map); // verify that an array within an array has been returned @@ -342,19 +284,12 @@ void RGBScript_Test::runScripts() uint(0) }; - RGBScriptsCache scripts(m_doc); - QVERIFY(scripts.load(QDir(INTERNAL_SCRIPTDIR))); - QStringList names = scripts.names(); - QVERIFY(names.contains(QStringLiteral("Audio Cellular"))); - QVERIFY(names.contains(QStringLiteral("Audio Fire"))); - QVERIFY(names.contains(QStringLiteral("Audio Fireworks"))); - QVERIFY(names.contains(QStringLiteral("Balls"))); - // Iterate the list of scripts + QStringList names = m_doc->rgbScriptsCache()->names(); foreach (QString name, names) { qDebug() << "Evaluating script" << name; - QScopedPointer s(scripts.script(name)); + QScopedPointer s(m_doc->rgbScriptsCache()->script(name)); QString fileName = s->fileName(); QString scriptName = fileName.split(QDir::separator()).last(); @@ -414,7 +349,7 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(rgbMap[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(rgbMap[y][x] <= 0xffffff); } } } @@ -438,7 +373,9 @@ void RGBScript_Test::runScripts() { if (s->acceptColors() > 0) { - QCOMPARE(rgbMap[y][x] & 0xff000000u, 0xff000000u); + // verify that the alpha channel is zero + QVERIFY((rgbMap[y][x] & 0xff000000) == 0); + QVERIFY((rgbMap[y][x] >> 16) <= 0x0000ff); if (!randomScript && 0 == step && 1 < s->acceptColors() && 2 < steps) { // if more than one color is accepted and the script has more than two steps - one per color, @@ -449,7 +386,7 @@ void RGBScript_Test::runScripts() } else { - QCOMPARE(rgbMap[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(rgbMap[y][x] <= 0x00ffffff); } } } @@ -503,7 +440,7 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(map[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(map[y][x] <= 0xffffff); } } } @@ -533,7 +470,7 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(map[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(map[y][x] <= 0xffffff); } } } @@ -552,20 +489,16 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(map[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(map[y][x] <= 0xffffff); } } } break; case RGBScriptProperty::Float: - { - const QString value = s->property(property.m_name); - bool ok = false; - value.toDouble(&ok); - QVERIFY(ok); - s->setProperty(property.m_name, value); + // Test with an integer value + s->setProperty(property.m_name, QString::number(-1024)); qDebug() << " Readback: " << s->property(property.m_name); - QCOMPARE(s->property(property.m_name), value); + QVERIFY(s->property(property.m_name) == QString::number(-1024)); for (int step = 0; step < realsteps; step++) { RGBMap map; @@ -577,12 +510,11 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(map[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(map[y][x] <= 0xffffff); } } } break; - } case RGBScriptProperty::String: // Test with an integer value s->setProperty(property.m_name, QString("QLC+")); @@ -599,7 +531,7 @@ void RGBScript_Test::runScripts() { for (int x = 0; x < mapSize.width(); x++) { - QCOMPARE(map[y][x] & 0xff000000u, 0xff000000u); + QVERIFY(map[y][x] <= 0xffffff); } } } diff --git a/qmlui/qml/fixturesfunctions/RGBMatrixEditor.qml b/qmlui/qml/fixturesfunctions/RGBMatrixEditor.qml index 9e239f0b72..e3b87bee2c 100644 --- a/qmlui/qml/fixturesfunctions/RGBMatrixEditor.qml +++ b/qmlui/qml/fixturesfunctions/RGBMatrixEditor.qml @@ -38,6 +38,15 @@ Rectangle signal requestView(int ID, string qmlSrc, bool back) + onAlgoColorsChanged: + { + var cCount = rgbMatrixEditor.algoColorsCount + color1Button.color = algoColors[0] + color2Button.color = cCount > 1 && rgbMatrixEditor.hasColorAtIndex(1) ? algoColors[1] : "transparent" + color3Button.color = cCount > 2 && rgbMatrixEditor.hasColorAtIndex(2) ? algoColors[2] : "transparent" + color4Button.color = cCount > 3 && rgbMatrixEditor.hasColorAtIndex(3) ? algoColors[3] : "transparent" + color5Button.color = cCount > 4 && rgbMatrixEditor.hasColorAtIndex(4) ? algoColors[4] : "transparent" + } TimeEditTool { @@ -89,7 +98,8 @@ Rectangle onToolColorChanged: function(r, g, b, w, a, uv) { - rgbMatrixEditor.setColorAtIndex(colorIndex, Qt.rgba(r, g, b, 1.0)) + previewBtn.color = Qt.rgba(r, g, b, 1.0) + rgbMatrixEditor.setColorAtIndex(colorIndex, previewBtn.color) } onClose: visible = false } @@ -210,19 +220,13 @@ Rectangle } } - Row + IconButton { - height: editorColumn.itemsHeight - spacing: 4 - - IconButton - { - width: UISettings.listItemHeight - height: width - imgSource: "qrc:/sequence.svg" - tooltip: qsTr("Save this matrix to a sequence") - onClicked: rgbMatrixEditor.saveToSequence() - } + width: UISettings.listItemHeight + height: width + imgSource: "qrc:/sequence.svg" + tooltip: qsTr("Save this matrix to a sequence") + onClicked: rgbMatrixEditor.saveToSequence() } // row 4 @@ -274,19 +278,19 @@ Rectangle { mLabel: qsTr("Amber") }, { mLabel: qsTr("UV") }, { mLabel: qsTr("Dimmer") }, - { mLabel: qsTr("Shutter") }, - { mLabel: qsTr("RGBW (Accurate)") }, - { mLabel: qsTr("RGBW (Brighter)") } + { mLabel: qsTr("Shutter") } ] currentIndex: rgbMatrixEditor.controlMode onCurrentIndexChanged: rgbMatrixEditor.controlMode = currentIndex } - // Brightness + // row 6 RobotoText { - label: qsTr("Brightness") + id: colorLabel + label: qsTr("Colors") + visible: rgbMatrixEditor.algoColorsCount > 0 ? true : false height: editorColumn.itemsHeight onWidthChanged: { @@ -294,246 +298,239 @@ Rectangle width = Qt.binding(function() { return editorColumn.firstColumnWidth }) } } - CustomDoubleSpinBox + + Row { + //width: editorColumn.colWidth Layout.columnSpan: 2 - Layout.fillWidth: true height: editorColumn.itemsHeight - decimals: 2 - realFrom: 0 - realTo: 1000 - realStep: 0.01 - realValue: rgbMatrixEditor.brightness - onRealValueChanged: rgbMatrixEditor.brightness = realValue - } + spacing: 4 - // Rotation - RobotoText - { - label: qsTr("Rotation") - height: editorColumn.itemsHeight - onWidthChanged: + Rectangle { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + id: color1Button + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: color1MouseArea.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + visible: rgbMatrixEditor.algoColorsCount > 0 ? true : false + + MouseArea + { + id: color1MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(0, color1Button) + } + } } - } - CustomComboBox - { - Layout.columnSpan: 2 - Layout.fillWidth: true - height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("None") }, - { mLabel: qsTr("90\u00B0 CW") }, - { mLabel: qsTr("180\u00B0") }, - { mLabel: qsTr("270\u00B0 CW") } - ] - currentIndex: rgbMatrixEditor.rotation - onCurrentIndexChanged: rgbMatrixEditor.rotation = currentIndex - } - - // Mirror - RobotoText - { - label: qsTr("Mirror") - height: editorColumn.itemsHeight - onWidthChanged: + Rectangle { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + width: UISettings.listItemHeight + height: width + color: "transparent" + visible: rgbMatrixEditor.algoColorsCount > 2 ? true : false } - } - CustomComboBox - { - Layout.columnSpan: 2 - Layout.fillWidth: true - height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("Off") }, - { mLabel: qsTr("Horizontal") }, - { mLabel: qsTr("Vertical") }, - { mLabel: qsTr("Both") } - ] - currentIndex: rgbMatrixEditor.mirror - onCurrentIndexChanged: rgbMatrixEditor.mirror = currentIndex - } - // Mirror Blend (only visible when mirror is active) - RobotoText - { - visible: rgbMatrixEditor.mirror > 0 - label: qsTr("Mirror blend") - height: editorColumn.itemsHeight - onWidthChanged: + Rectangle { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + id: color2Button + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: color2MouseArea.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + visible: rgbMatrixEditor.algoColorsCount > 1 ? true : false + + MouseArea + { + id: color2MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(1, color2Button) + } + } } - } - CustomComboBox - { - visible: rgbMatrixEditor.mirror > 0 - Layout.columnSpan: 2 - Layout.fillWidth: true - height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("Flip") }, - { mLabel: qsTr("Max") }, - { mLabel: qsTr("Average") }, - { mLabel: qsTr("Additive") } - ] - currentIndex: rgbMatrixEditor.mirrorBlend - onCurrentIndexChanged: rgbMatrixEditor.mirrorBlend = currentIndex - } - - // Beat Transform - Effect - RobotoText - { - label: qsTr("Beat effect") - height: editorColumn.itemsHeight - onWidthChanged: + IconButton { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + width: UISettings.listItemHeight + height: width + faSource: FontAwesome.fa_xmark + faColor: "darkred" + tooltip: qsTr("Reset color 2") + visible: rgbMatrixEditor.algoColorsCount > 1 ? true : false + onClicked: + { + color2Button.color = "transparent" + rgbMatrixEditor.resetColorAtIndex(1) + } } } - CustomComboBox + + // row 7 + Rectangle { - Layout.columnSpan: 2 - Layout.fillWidth: true + id: colorRow1 height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("Off") }, - { mLabel: qsTr("Mirror") }, - { mLabel: qsTr("Color Invert") }, - { mLabel: qsTr("Blackout") }, - { mLabel: qsTr("Whiteout") } - ] - currentIndex: rgbMatrixEditor.beatEffect - onCurrentIndexChanged: rgbMatrixEditor.beatEffect = currentIndex + width: colorLabel.width + color: "transparent" + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false } - // Beat Transform - Selection (only visible when effect != Off) - RobotoText + Row { - visible: rgbMatrixEditor.beatEffect > 0 - label: qsTr("Beat selection") + Layout.columnSpan: 2 + //width: editorColumn.colWidth height: editorColumn.itemsHeight - onWidthChanged: + spacing: 4 + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false + + Rectangle { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + id: color3Button + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: color3MouseArea.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + visible: rgbMatrixEditor.algoColorsCount > 2 ? true : false + + MouseArea + { + id: color3MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(2, color3Button) + } + } + } + IconButton + { + width: UISettings.listItemHeight + height: width + faSource: FontAwesome.fa_xmark + faColor: "darkred" + tooltip: qsTr("Reset color 3") + visible: rgbMatrixEditor.algoColorsCount > 2 ? true : false + onClicked: + { + color3Button.color = "transparent" + rgbMatrixEditor.resetColorAtIndex(2) + } } - } - CustomComboBox - { - visible: rgbMatrixEditor.beatEffect > 0 - Layout.columnSpan: 2 - Layout.fillWidth: true - height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("All on downbeat") }, - { mLabel: qsTr("Walk") }, - { mLabel: qsTr("Random") } - ] - currentIndex: rgbMatrixEditor.beatSelection - onCurrentIndexChanged: rgbMatrixEditor.beatSelection = currentIndex - } - // Beat Transform - Orientation (only visible when effect != Off) - RobotoText - { - visible: rgbMatrixEditor.beatEffect > 0 - label: qsTr("Beat orientation") - height: editorColumn.itemsHeight - onWidthChanged: + Rectangle { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + id: color4Button + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: color4MouseArea.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + visible: rgbMatrixEditor.algoColorsCount > 3 ? true : false + + MouseArea + { + id: color4MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(3, color4Button) + } + } + } + IconButton + { + width: UISettings.listItemHeight + height: width + faSource: FontAwesome.fa_xmark + faColor: "darkred" + tooltip: qsTr("Reset color 4") + visible: rgbMatrixEditor.algoColorsCount > 3 ? true : false + onClicked: + { + color4Button.color = "transparent" + rgbMatrixEditor.resetColorAtIndex(3) + } } } - CustomComboBox - { - visible: rgbMatrixEditor.beatEffect > 0 - Layout.columnSpan: 2 - Layout.fillWidth: true - height: editorColumn.itemsHeight - model: [ - { mLabel: qsTr("Rows") }, - { mLabel: qsTr("Columns") } - ] - currentIndex: rgbMatrixEditor.beatOrientation - onCurrentIndexChanged: rgbMatrixEditor.beatOrientation = currentIndex - } - - // row 6: dynamic color pickers (one per algoColorsCount) - RobotoText + // row 8 + Rectangle { - id: colorLabel - label: qsTr("Colors") - visible: rgbMatrixEditor.algoColorsCount > 0 + id: colorRow2 height: editorColumn.itemsHeight - onWidthChanged: - { - editorColumn.checkLabelWidth(width) - width = Qt.binding(function() { return editorColumn.firstColumnWidth }) - } + width: colorLabel.width + color: "transparent" + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false } - Flow + Row { Layout.columnSpan: 2 - Layout.fillWidth: true - spacing: 6 - visible: rgbMatrixEditor.algoColorsCount > 0 + //width: editorColumn.colWidth + height: editorColumn.itemsHeight + spacing: 4 + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false - Repeater + Rectangle { - model: Math.min(rgbMatrixEditor.algoColorsCount, 5) - - delegate: Row + id: color5Button + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: color5MouseArea.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false + + MouseArea { - spacing: 2 - - Rectangle - { - id: colorBtn - width: UISettings.iconSizeDefault * 2 - height: editorColumn.itemsHeight - radius: 5 - border.color: colorMA.containsMouse ? "white" : UISettings.bgLight - border.width: 2 - color: rgbmeContainer.algoColors && rgbMatrixEditor.hasColorAtIndex(index) - ? rgbmeContainer.algoColors[index] : "transparent" - - MouseArea - { - id: colorMA - anchors.fill: parent - hoverEnabled: true - onClicked: - { - if (colorTool.visible) - colorTool.hide() - else - colorTool.showTool(index, colorBtn) - } - } - } - IconButton + id: color5MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { - width: UISettings.listItemHeight - height: width - faSource: FontAwesome.fa_xmark - faColor: "darkred" - tooltip: qsTr("Reset color %1").arg(index + 1) - visible: index > 0 - onClicked: rgbMatrixEditor.resetColorAtIndex(index) + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(4, color5Button) } } } + IconButton + { + width: UISettings.listItemHeight + height: width + faSource: FontAwesome.fa_xmark + faColor: "darkred" + tooltip: qsTr("Reset color 5") + visible: rgbMatrixEditor.algoColorsCount > 4 ? true : false + onClicked: + { + color5Button.color = "transparent" + rgbMatrixEditor.resetColorAtIndex(4) + } + } } } @@ -563,7 +560,7 @@ Rectangle function showTimeTool(item, titleLabel, timeLabel, type) { - timeEditTool.allowFractions = QLCFunction.FineFractions + timeEditTool.allowFractions = QLCFunction.ByTwoFractions timeEditTool.show(-1, item.mapToItem(mainView, 0, 0).y - timeEditTool.height, titleLabel, timeLabel, type) } @@ -704,35 +701,6 @@ Rectangle width: UISettings.listItemHeight height: width } - - // Row 5 - RobotoText - { - label: qsTr("Step duration") - height: UISettings.listItemHeight - } - Rectangle - { - Layout.fillWidth: true - height: UISettings.listItemHeight - color: UISettings.bgMedium - - RobotoText - { - x: 3 - height: parent.height - label: { - var dur = rgbMatrixEditor.duration - if (dur === -2) return "∞" - return TimeUtils.timeToQlcString(dur, rgbMatrixEditor.tempoType) - } - } - } - Item - { - width: UISettings.listItemHeight - height: width - } } } diff --git a/qmlui/rgbmatrixeditor.cpp b/qmlui/rgbmatrixeditor.cpp index 77482f2e46..a3281f423a 100644 --- a/qmlui/rgbmatrixeditor.cpp +++ b/qmlui/rgbmatrixeditor.cpp @@ -199,9 +199,7 @@ void RGBMatrixEditor::setColorAtIndex(int index, QColor color) if (m_matrix == nullptr || m_matrix->getColor(index) == color) return; - if (m_matrix->controlMode() != RGBMatrix::ControlModeRgb && - m_matrix->controlMode() != RGBMatrix::ControlModeRgbw && - m_matrix->controlMode() != RGBMatrix::ControlModeRgbwBrighter) + if (m_matrix->controlMode() != RGBMatrix::ControlModeRgb) { // Convert color to grayscale for non-RGB control modes uchar gray = qGray(color.rgb()); @@ -225,7 +223,6 @@ void RGBMatrixEditor::resetColorAtIndex(int index) m_matrix->setColor(index, QColor()); if (index < 2) m_previewStepHandler->calculateColorDelta(m_matrix->getColor(0), m_matrix->getColor(1), m_matrix->algorithm()); - emit algoColorsChanged(); } bool RGBMatrixEditor::hasColorAtIndex(int index) const @@ -246,9 +243,7 @@ void RGBMatrixEditor::updateColors() return; if (m_matrix->blendMode() == Universe::MaskBlend || - (m_matrix->controlMode() != RGBMatrix::ControlModeRgb && - m_matrix->controlMode() != RGBMatrix::ControlModeRgbw && - m_matrix->controlMode() != RGBMatrix::ControlModeRgbwBrighter)) + m_matrix->controlMode() != RGBMatrix::ControlModeRgb) { m_matrix->setColor(0, Qt::white); // Overwrite more colors only if applied. @@ -613,143 +608,6 @@ void RGBMatrixEditor::setControlMode(int mode) emit algoColorsChanged(); } -/************************************************************************ - * Rotation & Mirror - ************************************************************************/ - -int RGBMatrixEditor::rotation() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->rotation(); -} - -void RGBMatrixEditor::setRotation(int r) -{ - if (m_matrix == nullptr || r == m_matrix->rotation()) - return; - m_matrix->setRotation(r); - initPreviewData(); - emit rotationChanged(); -} - -int RGBMatrixEditor::mirror() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->mirror(); -} - -void RGBMatrixEditor::setMirror(int m) -{ - if (m_matrix == nullptr || m == m_matrix->mirror()) - return; - m_matrix->setMirror(m); - initPreviewData(); - emit mirrorChanged(); -} - -int RGBMatrixEditor::mirrorBlend() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->mirrorBlend(); -} - -void RGBMatrixEditor::setMirrorBlend(int b) -{ - if (m_matrix == nullptr || b == m_matrix->mirrorBlend()) - return; - m_matrix->setMirrorBlend(RGBMatrix::MirrorBlend(b)); - initPreviewData(); - emit mirrorBlendChanged(); -} - -qreal RGBMatrixEditor::brightness() const -{ - if (m_matrix == nullptr) - return 1.0; - return m_matrix->brightness(); -} - -void RGBMatrixEditor::setBrightness(qreal b) -{ - if (m_matrix == nullptr || qFuzzyCompare(b, m_matrix->brightness())) - return; - m_matrix->setBrightness(b); - emit brightnessChanged(); -} - -/************************************************************************ - * Beat Transform - ************************************************************************/ - -int RGBMatrixEditor::beatEffect() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->beatEffect(); -} - -void RGBMatrixEditor::setBeatEffect(int e) -{ - if (m_matrix == nullptr || e == m_matrix->beatEffect()) - return; - m_matrix->setBeatEffect(RGBMatrix::BeatEffect(e)); - initPreviewData(); - emit beatEffectChanged(); -} - -int RGBMatrixEditor::beatSelection() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->beatSelection(); -} - -void RGBMatrixEditor::setBeatSelection(int s) -{ - if (m_matrix == nullptr || s == m_matrix->beatSelection()) - return; - m_matrix->setBeatSelection(RGBMatrix::BeatSelection(s)); - initPreviewData(); - emit beatSelectionChanged(); -} - -int RGBMatrixEditor::beatOrientation() const -{ - if (m_matrix == nullptr) - return 0; - return m_matrix->beatOrientation(); -} - -void RGBMatrixEditor::setBeatOrientation(int o) -{ - if (m_matrix == nullptr || o == m_matrix->beatOrientation()) - return; - m_matrix->setBeatOrientation(RGBMatrix::BeatOrientation(o)); - initPreviewData(); - emit beatOrientationChanged(); -} - -/************************************************************************ - * Audio Routing - ************************************************************************/ - -bool RGBMatrixEditor::algorithmUsesAudio() const -{ - if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) - return false; - return m_matrix->algorithm()->usesAudio(); -} - -QStringList RGBMatrixEditor::audioInputCategories() const -{ - if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) - return {}; - return m_matrix->algorithm()->audioInputCategories(); -} - /************************************************************************ * Save to Sequence ************************************************************************/ @@ -992,21 +850,13 @@ void RGBMatrixEditor::slotPreviewTimeout() { m_previewElapsed += MasterTimer::tick(); } - else if (m_matrix->tempoType() == Function::Beats) - { - m_previewElapsed += MasterTimer::tick(); - } - - uint effectiveDuration = m_matrix->duration(); - if (m_matrix->tempoType() == Function::Beats) + else if (m_matrix->tempoType() == Function::Beats && m_gotBeat) { - int beatDur = m_doc->masterTimer()->beatTimeDuration(); - effectiveDuration = Function::beatsToTime(effectiveDuration, beatDur); - if (effectiveDuration == 0) - effectiveDuration = MasterTimer::tick(); + m_gotBeat = false; + m_previewElapsed += 1000; } - if (m_previewElapsed >= effectiveDuration) + if (m_previewElapsed >= m_matrix->duration()) { QMutexLocker locker(&m_previewMutex); diff --git a/qmlui/rgbmatrixeditor.h b/qmlui/rgbmatrixeditor.h index 2f7bf1323d..6300fcc3ed 100644 --- a/qmlui/rgbmatrixeditor.h +++ b/qmlui/rgbmatrixeditor.h @@ -44,18 +44,6 @@ class RGBMatrixEditor final : public FunctionEditor Q_PROPERTY(int blendMode READ blendMode WRITE setBlendMode NOTIFY blendModeChanged) Q_PROPERTY(int controlMode READ controlMode WRITE setControlMode NOTIFY controlModeChanged) - Q_PROPERTY(int rotation READ rotation WRITE setRotation NOTIFY rotationChanged) - Q_PROPERTY(int mirror READ mirror WRITE setMirror NOTIFY mirrorChanged) - Q_PROPERTY(int mirrorBlend READ mirrorBlend WRITE setMirrorBlend NOTIFY mirrorBlendChanged) - Q_PROPERTY(qreal brightness READ brightness WRITE setBrightness NOTIFY brightnessChanged) - - Q_PROPERTY(int beatEffect READ beatEffect WRITE setBeatEffect NOTIFY beatEffectChanged) - Q_PROPERTY(int beatSelection READ beatSelection WRITE setBeatSelection NOTIFY beatSelectionChanged) - Q_PROPERTY(int beatOrientation READ beatOrientation WRITE setBeatOrientation NOTIFY beatOrientationChanged) - - Q_PROPERTY(bool algorithmUsesAudio READ algorithmUsesAudio NOTIFY algorithmIndexChanged) - Q_PROPERTY(QStringList audioInputCategories READ audioInputCategories NOTIFY algorithmIndexChanged) - // Text Algorithm specific properties Q_PROPERTY(QString algoText READ algoText WRITE setAlgoText NOTIFY algoTextChanged) Q_PROPERTY(QFont algoTextFont READ algoTextFont WRITE setAlgoTextFont NOTIFY algoTextFontChanged) @@ -174,53 +162,6 @@ class RGBMatrixEditor final : public FunctionEditor signals: void controlModeChanged(); - /************************************************************************ - * Rotation & Mirror - ************************************************************************/ -public: - int rotation() const; - void setRotation(int r); - - int mirror() const; - void setMirror(int m); - - int mirrorBlend() const; - void setMirrorBlend(int b); - - qreal brightness() const; - void setBrightness(qreal b); - -signals: - void rotationChanged(); - void mirrorChanged(); - void mirrorBlendChanged(); - void brightnessChanged(); - - /************************************************************************ - * Beat Transform - ************************************************************************/ -public: - int beatEffect() const; - void setBeatEffect(int e); - - int beatSelection() const; - void setBeatSelection(int s); - - int beatOrientation() const; - void setBeatOrientation(int o); - -signals: - void beatEffectChanged(); - void beatSelectionChanged(); - void beatOrientationChanged(); - - /************************************************************************ - * Audio Routing - ************************************************************************/ -public: - bool algorithmUsesAudio() const; - QStringList audioInputCategories() const; - /************************************************************************ * Preview ************************************************************************/ diff --git a/resources/rgbscripts/audioaurora.js b/resources/huescripts/audioaurora.js similarity index 100% rename from resources/rgbscripts/audioaurora.js rename to resources/huescripts/audioaurora.js diff --git a/resources/rgbscripts/audiobarcode.js b/resources/huescripts/audiobarcode.js similarity index 100% rename from resources/rgbscripts/audiobarcode.js rename to resources/huescripts/audiobarcode.js diff --git a/resources/rgbscripts/audiobasslaser.js b/resources/huescripts/audiobasslaser.js similarity index 100% rename from resources/rgbscripts/audiobasslaser.js rename to resources/huescripts/audiobasslaser.js diff --git a/resources/rgbscripts/audiobeatcolors.js b/resources/huescripts/audiobeatcolors.js similarity index 100% rename from resources/rgbscripts/audiobeatcolors.js rename to resources/huescripts/audiobeatcolors.js diff --git a/resources/rgbscripts/audioblocks.js b/resources/huescripts/audioblocks.js similarity index 100% rename from resources/rgbscripts/audioblocks.js rename to resources/huescripts/audioblocks.js diff --git a/resources/rgbscripts/audioblurz.js b/resources/huescripts/audioblurz.js similarity index 100% rename from resources/rgbscripts/audioblurz.js rename to resources/huescripts/audioblurz.js diff --git a/resources/rgbscripts/audiobuildup.js b/resources/huescripts/audiobuildup.js similarity index 100% rename from resources/rgbscripts/audiobuildup.js rename to resources/huescripts/audiobuildup.js diff --git a/resources/rgbscripts/audiocellular.js b/resources/huescripts/audiocellular.js similarity index 100% rename from resources/rgbscripts/audiocellular.js rename to resources/huescripts/audiocellular.js diff --git a/resources/rgbscripts/audiochaser.js b/resources/huescripts/audiochaser.js similarity index 100% rename from resources/rgbscripts/audiochaser.js rename to resources/huescripts/audiochaser.js diff --git a/resources/rgbscripts/audiocrawler.js b/resources/huescripts/audiocrawler.js similarity index 100% rename from resources/rgbscripts/audiocrawler.js rename to resources/huescripts/audiocrawler.js diff --git a/resources/rgbscripts/audiodjlight.js b/resources/huescripts/audiodjlight.js similarity index 100% rename from resources/rgbscripts/audiodjlight.js rename to resources/huescripts/audiodjlight.js diff --git a/resources/rgbscripts/audioenergy.js b/resources/huescripts/audioenergy.js similarity index 100% rename from resources/rgbscripts/audioenergy.js rename to resources/huescripts/audioenergy.js diff --git a/resources/rgbscripts/audioenergy2.js b/resources/huescripts/audioenergy2.js similarity index 100% rename from resources/rgbscripts/audioenergy2.js rename to resources/huescripts/audioenergy2.js diff --git a/resources/rgbscripts/audioequalizer.js b/resources/huescripts/audioequalizer.js similarity index 100% rename from resources/rgbscripts/audioequalizer.js rename to resources/huescripts/audioequalizer.js diff --git a/resources/rgbscripts/audiofire.js b/resources/huescripts/audiofire.js similarity index 100% rename from resources/rgbscripts/audiofire.js rename to resources/huescripts/audiofire.js diff --git a/resources/rgbscripts/audiofireworks.js b/resources/huescripts/audiofireworks.js similarity index 100% rename from resources/rgbscripts/audiofireworks.js rename to resources/huescripts/audiofireworks.js diff --git a/resources/rgbscripts/audioflowfield.js b/resources/huescripts/audioflowfield.js similarity index 100% rename from resources/rgbscripts/audioflowfield.js rename to resources/huescripts/audioflowfield.js diff --git a/resources/rgbscripts/audioglitch.js b/resources/huescripts/audioglitch.js similarity index 100% rename from resources/rgbscripts/audioglitch.js rename to resources/huescripts/audioglitch.js diff --git a/resources/rgbscripts/audioglitch2.js b/resources/huescripts/audioglitch2.js similarity index 100% rename from resources/rgbscripts/audioglitch2.js rename to resources/huescripts/audioglitch2.js diff --git a/resources/rgbscripts/audiogravimeter.js b/resources/huescripts/audiogravimeter.js similarity index 100% rename from resources/rgbscripts/audiogravimeter.js rename to resources/huescripts/audiogravimeter.js diff --git a/resources/rgbscripts/audiohueshift.js b/resources/huescripts/audiohueshift.js similarity index 100% rename from resources/rgbscripts/audiohueshift.js rename to resources/huescripts/audiohueshift.js diff --git a/resources/rgbscripts/audiolava.js b/resources/huescripts/audiolava.js similarity index 100% rename from resources/rgbscripts/audiolava.js rename to resources/huescripts/audiolava.js diff --git a/resources/rgbscripts/audiomelt.js b/resources/huescripts/audiomelt.js similarity index 100% rename from resources/rgbscripts/audiomelt.js rename to resources/huescripts/audiomelt.js diff --git a/resources/rgbscripts/audiomeltsparkle.js b/resources/huescripts/audiomeltsparkle.js similarity index 100% rename from resources/rgbscripts/audiomeltsparkle.js rename to resources/huescripts/audiomeltsparkle.js diff --git a/resources/rgbscripts/audioplasma.js b/resources/huescripts/audioplasma.js similarity index 100% rename from resources/rgbscripts/audioplasma.js rename to resources/huescripts/audioplasma.js diff --git a/resources/rgbscripts/audiopower.js b/resources/huescripts/audiopower.js similarity index 100% rename from resources/rgbscripts/audiopower.js rename to resources/huescripts/audiopower.js diff --git a/resources/rgbscripts/audiopuddles.js b/resources/huescripts/audiopuddles.js similarity index 100% rename from resources/rgbscripts/audiopuddles.js rename to resources/huescripts/audiopuddles.js diff --git a/resources/rgbscripts/audioreaction.js b/resources/huescripts/audioreaction.js similarity index 100% rename from resources/rgbscripts/audioreaction.js rename to resources/huescripts/audioreaction.js diff --git a/resources/rgbscripts/audioreactor.js b/resources/huescripts/audioreactor.js similarity index 100% rename from resources/rgbscripts/audioreactor.js rename to resources/huescripts/audioreactor.js diff --git a/resources/rgbscripts/audioscan.js b/resources/huescripts/audioscan.js similarity index 100% rename from resources/rgbscripts/audioscan.js rename to resources/huescripts/audioscan.js diff --git a/resources/rgbscripts/audioscanflare.js b/resources/huescripts/audioscanflare.js similarity index 100% rename from resources/rgbscripts/audioscanflare.js rename to resources/huescripts/audioscanflare.js diff --git a/resources/rgbscripts/audioscanmulti.js b/resources/huescripts/audioscanmulti.js similarity index 100% rename from resources/rgbscripts/audioscanmulti.js rename to resources/huescripts/audioscanmulti.js diff --git a/resources/rgbscripts/audioshockwave.js b/resources/huescripts/audioshockwave.js similarity index 100% rename from resources/rgbscripts/audioshockwave.js rename to resources/huescripts/audioshockwave.js diff --git a/resources/rgbscripts/audioshot.js b/resources/huescripts/audioshot.js similarity index 100% rename from resources/rgbscripts/audioshot.js rename to resources/huescripts/audioshot.js diff --git a/resources/rgbscripts/audiosoap.js b/resources/huescripts/audiosoap.js similarity index 100% rename from resources/rgbscripts/audiosoap.js rename to resources/huescripts/audiosoap.js diff --git a/resources/rgbscripts/audiospectrum.js b/resources/huescripts/audiospectrum.js similarity index 100% rename from resources/rgbscripts/audiospectrum.js rename to resources/huescripts/audiospectrum.js diff --git a/resources/rgbscripts/audiosplittower.js b/resources/huescripts/audiosplittower.js similarity index 100% rename from resources/rgbscripts/audiosplittower.js rename to resources/huescripts/audiosplittower.js diff --git a/resources/rgbscripts/audiostrobe.js b/resources/huescripts/audiostrobe.js similarity index 100% rename from resources/rgbscripts/audiostrobe.js rename to resources/huescripts/audiostrobe.js diff --git a/resources/rgbscripts/audiotunnel.js b/resources/huescripts/audiotunnel.js similarity index 100% rename from resources/rgbscripts/audiotunnel.js rename to resources/huescripts/audiotunnel.js diff --git a/resources/rgbscripts/audiovortex.js b/resources/huescripts/audiovortex.js similarity index 100% rename from resources/rgbscripts/audiovortex.js rename to resources/huescripts/audiovortex.js diff --git a/resources/rgbscripts/audiowater.js b/resources/huescripts/audiowater.js similarity index 100% rename from resources/rgbscripts/audiowater.js rename to resources/huescripts/audiowater.js diff --git a/resources/rgbscripts/hsvutil.js b/resources/huescripts/hsvutil.js similarity index 100% rename from resources/rgbscripts/hsvutil.js rename to resources/huescripts/hsvutil.js diff --git a/resources/rgbscripts/CMakeLists.txt b/resources/rgbscripts/CMakeLists.txt index 423804a992..4acd60e299 100644 --- a/resources/rgbscripts/CMakeLists.txt +++ b/resources/rgbscripts/CMakeLists.txt @@ -2,47 +2,6 @@ project(scripts) set(SCRIPT_FILES alternate.js - audioaurora.js - audiobarcode.js - audiobasslaser.js - audiobeatcolors.js - audiobuildup.js - audioblocks.js - audioblurz.js - audiocellular.js - audiochaser.js - audiocrawler.js - audiodjlight.js - audioenergy.js - audioenergy2.js - audioequalizer.js - audiofire.js - audiofireworks.js - audioflowfield.js - audioglitch.js - audioglitch2.js - audiogravimeter.js - audiohueshift.js - audiolava.js - audiomelt.js - audiomeltsparkle.js - audioplasma.js - audiopower.js - audiopuddles.js - audioreaction.js - audioreactor.js - audioscan.js - audioscanflare.js - audioscanmulti.js - audioshockwave.js - audioshot.js - audiosoap.js - audiospectrum.js - audiosplittower.js - audiostrobe.js - audiotunnel.js - audiovortex.js - audiowater.js balls.js blinder.js circles.js @@ -70,7 +29,6 @@ set(SCRIPT_FILES randompixelperrowmulticolor.js randomrow.js randomsingle.js - hsvutil.js sinewave.js snowbubbles.js squares.js @@ -97,16 +55,4 @@ if(ANDROID) endforeach() endif() -# Create symlinks in build directory for dev builds (out-of-source only) -get_filename_component(_src_real "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) -get_filename_component(_bld_real "${CMAKE_CURRENT_BINARY_DIR}" REALPATH) -if(NOT ANDROID AND NOT _src_real STREQUAL _bld_real) - foreach(SCRIPT_FILE IN LISTS SCRIPT_FILES) - if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${SCRIPT_FILE}") - file(CREATE_LINK "${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT_FILE}" - "${CMAKE_CURRENT_BINARY_DIR}/${SCRIPT_FILE}" SYMBOLIC) - endif() - endforeach() -endif() - install(FILES ${SCRIPT_FILES} DESTINATION ${INSTALLROOT}/${RGBSCRIPTDIR}) diff --git a/resources/rgbscripts/alternate.js b/resources/rgbscripts/alternate.js index aec08c8715..be9aa487f8 100644 --- a/resources/rgbscripts/alternate.js +++ b/resources/rgbscripts/alternate.js @@ -105,37 +105,39 @@ var testAlgo; }; var util = new Object; + util.colorArray = new Array(algo.acceptColors); util.getRawColor = function (idx) { - if (Array.isArray(algo.colors) && algo.colors.length > idx && algo.colors[idx]) { - return algo.colors[idx]; + var color = 0; + if (Array.isArray(util.colorArray) && util.colorArray.length > idx && util.colorArray[idx]) { + color = util.colorArray[idx]; } - return {h: 0, s: 0, v: 0}; - }; - - algo.rgbMapSetColors = function(rawColors) {}; - - algo.rgbMapGetColors = function() { return []; }; - - function writeColor(map, width, x, y, color) { - var i = (y * width + x) * 3; - map[i] = color.h; map[i + 1] = color.s; map[i + 2] = color.v; + return color; } - function copyPixel(map, width, srcX, srcY, dstX, dstY) { - var si = (srcY * width + srcX) * 3; - var di = (dstY * width + dstX) * 3; - map[di] = map[si]; map[di + 1] = map[si + 1]; map[di + 2] = map[si + 2]; + algo.rgbMapSetColors = function(rawColors) + { + if (! Array.isArray(rawColors)) + return; + for (var i = 0; i < algo.acceptColors; i++) { + if (i < rawColors.length) + { + util.colorArray[i] = rawColors[i]; + } else { + util.colorArray[i] = 0; + } + } } algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); var colorSelectOne = (step === 1) ? false : true; var rowColorOne = colorSelectOne; var realBlockSize = algo.blockSize; // Setup the rgb map for (y = 0; y < height; y++) { + map[y] = new Array(width); } var xMax = width; @@ -225,9 +227,9 @@ var testAlgo; } } if (colorSelectOne) { - writeColor(map, width, x, y, util.getRawColor(0)); + map[y][x] = util.getRawColor(0); } else { - writeColor(map, width, x, y, util.getRawColor(1)); + map[y][x] = util.getRawColor(1); } } } @@ -236,21 +238,21 @@ var testAlgo; if (algo.orientation === 0) { for (y = 0; y < yMax; y++) { for (x = 0; x < xMax; x++) { - copyPixel(map, width, x, y, width - x - 1, y); + map[y][width - x - 1] = map[y][x]; } } } else if (algo.orientation === 1) { for (y = 0; y < yMax; y++) { for (x = 0; x < xMax; x++) { - copyPixel(map, width, x, y, x, height - y - 1); + map[height - y - 1][x] = map[y][x]; } } } else if (algo.orientation === 2) { for (y = 0; y < yMax; y++) { for (x = 0; x < xMax; x++) { - copyPixel(map, width, x, y, x, height - y - 1); - copyPixel(map, width, x, y, width - x - 1, y); - copyPixel(map, width, x, y, width - x - 1, height - y - 1); + map[height - y - 1][x] = map[y][x]; + map[y][width - x - 1] = map[y][x]; + map[height - y - 1][width - x - 1] = map[y][x]; } } } diff --git a/resources/rgbscripts/balls.js b/resources/rgbscripts/balls.js index 33a1b565d0..da5f490221 100644 --- a/resources/rgbscripts/balls.js +++ b/resources/rgbscripts/balls.js @@ -52,27 +52,15 @@ var testAlgo; var util = new Object; util.colorArray = new Array(algo.acceptColors); - function addPixel(map, idx, h, s, v) { - v = Math.max(0, Math.min(1, v)); - if (v <= 0) return; - var existV = map[idx + 2]; - if (existV <= 0) { - map[idx] = h; map[idx + 1] = s; map[idx + 2] = v; - } else { - if (v > existV) { map[idx] = h; map[idx + 1] = s; } - map[idx + 2] = Math.min(1, existV + v); - } - } - algo.setSize = function (_size) { - algo.presetSize = parseInt(_size); + algo.presetSize = _size; }; algo.getSize = function () { return algo.presetSize; }; algo.setNumber = function (_step) { - algo.presetNumber = parseInt(_step); + algo.presetNumber = _step; algo.initialized = false; }; algo.getNumber = function () { @@ -88,10 +76,9 @@ var testAlgo; }; util.getRawColor = function (idx) { - var colors = (algo.presetIndex === 0 && Array.isArray(algo.colors) && algo.colors.length > 0) - ? algo.colors : util.colorArray; - if (colors.length === 0) return {h: 0, s: 0, v: 0}; - return colors[idx % colors.length] || {h: 0, s: 0, v: 0}; + idx = idx % util.colorArray.length; + var color = util.colorArray[idx]; + return color; } algo.setPreset = function(_preset) @@ -101,13 +88,7 @@ var testAlgo; { algo.presetIndex = 0; algo.acceptColors = 5; - util.colorArray = [ - {h: 0.333, s: 1, v: 1}, - {h: 0.111, s: 1, v: 1}, - {h: 0.667, s: 1, v: 1}, - {h: 0.167, s: 1, v: 1}, - {h: 0.556, s: 1, v: 1} - ]; + util.colorArray = [ 0x00FF00, 0xFFAA00, 0x0000FF, 0xFFFF00, 0x00AAFF ]; } else if (_preset === "Random") { @@ -115,11 +96,13 @@ var testAlgo; util.colorArray = new Array(); for (var i = 0; i < algo.presetNumber; i++) { - util.colorArray[i] = { - h: Math.random(), - s: 0.7 + Math.random() * 0.3, - v: 0.7 + Math.random() * 0.3 - }; + do + { + var ballR = Math.round(Math.random() * 255); // Chose random + var ballG = Math.round(Math.random() * 255); // colour for + var ballB = Math.round(Math.random() * 255); // each Ball + } while ((ballR + ballG + ballB) < 356); // if it it to dim try again + util.colorArray[i] = (ballR << 16) + (ballG << 8) + ballB; } } else { algo.presetIndex = 0; } @@ -135,11 +118,23 @@ var testAlgo; algo.rgbMapSetColors = function(rawColors) { + if (! Array.isArray(rawColors)) + return; + if (algo.acceptColors > 0) + util.colorArray = Array(); + for (var i = 0; i < algo.acceptColors; i++) { + var isNumber = (rawColors[i] === rawColors[i]); + var color = rawColors[i]; + if (i < rawColors.length && isNumber) + { + util.colorArray.push(color); + } + } } algo.rgbMapGetColors = function() { - return []; + return util.colorArray; } util.initialize = function (width, height) { @@ -147,10 +142,10 @@ var testAlgo; algo.direction = new Array(algo.presetNumber); for (var i = 0; i < algo.presetNumber; i++) { - var x = Math.random() * (width - 1); - var y = Math.random() * (height - 1); + var x = Math.random() * (width - 1); // set random start + var y = Math.random() * (height - 1); // locations for balls algo.ball[i] = [y, x]; - var yDirection = (Math.random() * 2) - 1; + var yDirection = (Math.random() * 2) - 1; // and random directions var xDirection = (Math.random() * 2) - 1; algo.direction[i] = [yDirection, xDirection]; } @@ -163,44 +158,68 @@ var testAlgo; util.initialize(width, height); } - var map = HSVUtil.createMap(width, height); - - for (var i = 0; i < algo.presetNumber; i++) { - var color = util.getRawColor(i); - var yx = algo.ball[i]; - var step = algo.direction[i]; - var my = Math.floor(yx[0]); - var mx = Math.floor(yx[1]); - var boxSize = Math.round(algo.presetSize / 2); + var map = new Array(height); // Clear map data + for (var y = 0; y < height; y++) { + map[y] = new Array(); - for (var ry = my - boxSize; ry < my + boxSize + 2; ry++) { - - for (var rx = mx - boxSize; rx < mx + boxSize + 2; rx++) { + for (var x = 0; x < width; x++) { + map[y][x] = 0; + } + } - if (rx < width && rx > -1 && ry < height && ry > -1) { - var offx = rx - yx[1]; - var offy = ry - yx[0]; + for (var i = 0; i < algo.presetNumber; i++) { // for each ball displayed + rgb = util.getRawColor(i); // use RGB for ball random colour + var r = (rgb >> 16) & 0x00FF; // split colour in to + var g = (rgb >> 8) & 0x00FF; // separate parts + var b = rgb & 0x00FF; + var yx = algo.ball[i]; // ball's location, as float + var step = algo.direction[i]; // ball's direction / speed, as float + var my = Math.floor(yx[0]); // workout closest map location for ball + var mx = Math.floor(yx[1]); + var boxSize = Math.round(algo.presetSize / 2); // area size to draw ball + + for (var ry = my - boxSize; ry < my + boxSize + 2; ry++) { // area for faded edges + + for (var rx = mx - boxSize; rx < mx + boxSize + 2; rx++) { // to display ball + + if (rx < width && rx > -1 && ry < height && ry > -1) { // if edges are off the map dont draw + var pointRGB = map[ry][rx]; // get curent colour on the map + var pointr = (pointRGB >> 16) & 0x00FF;// so that colours mix and don't over + var pointg = (pointRGB >> 8) & 0x00FF; // write. + var pointb = pointRGB & 0x00FF; // splt rgb in to components + var ballr = r; + var ballg = g; + var ballb = b; + var offx = rx - yx[1]; // calculate the off set differance of map location + var offy = ry - yx[0]; // to the float location of the ball, using the hypotenuse var hyp = 1 - (Math.sqrt((offx * offx) + (offy * offy)) / ((algo.presetSize / 2) + 1)); - if (hyp < 0) { hyp = 0; } + if (hyp < 0) { hyp = 0; } // if the distance multiplyed by ball size is negative = 0 + pointr += Math.round(ballr * hyp); // dim mapped ball colours by the distance from + pointg += Math.round(ballg * hyp); // the ball center ( hyp = 1, full colour / 0, off) + pointb += Math.round(ballb * hyp); // add the ball colour to the mapped location + if (pointr > 255) { pointr = 255; } // if addind the colours over saturates + if (pointg > 255) { pointg = 255; } // reduce it to the maximum + if (pointb > 255) { pointb = 255; } + + pointRGB = (pointr << 16) + (pointg << 8) + pointb; // combine colours - var idx = (ry * width + rx) * 3; - var newV = color.v * hyp; - addPixel(map, idx, color.h, color.s, newV); + map[ry][rx] = pointRGB; // set mapped point } } } - if (algo.presetCollision === 0) { - for (var ti = 0; ti < algo.presetNumber; ti++) { + if (algo.presetCollision === 0) { // if colision detection is on + // Ball collision detection + for (var ti = 0; ti < algo.presetNumber; ti++) { // check all balls - if (ti !== i) { - var disy = (yx[0] + step[0]) - algo.ball[ti][0]; - var disx = (yx[1] + step[1]) - algo.ball[ti][1]; + if (ti !== i) { // but not the current one + var disy = (yx[0] + step[0]) - algo.ball[ti][0]; // calculate distance + var disx = (yx[1] + step[1]) - algo.ball[ti][1]; // to current ball var dish = Math.sqrt((disx * disx) + (disy * disy)); - if (dish < (1.414) * (algo.presetSize / 2)) { - var stepy = step[0]; - var stepx = step[1]; + if (dish < (1.414) * (algo.presetSize / 2)) { // if to close + var stepy = step[0]; // swap speed / direction of current ball + var stepx = step[1]; // with ball that is to close algo.direction[i][0] = algo.direction[ti][0]; algo.direction[i][1] = algo.direction[ti][1]; algo.direction[ti][0] = stepy; @@ -210,22 +229,24 @@ var testAlgo; } } - if (yx[0] <= 0 && step[0] < 0) { step[0] *= -1; } - else if (yx[0] >= height - 1 && step[0] > 0) { step[0] *= -1; } + // edge collision detection + if (yx[0] <= 0 && step[0] < 0) { step[0] *= -1; } // top edge and moving up + else if (yx[0] >= height - 1 && step[0] > 0) { step[0] *= -1; } // bottom edge and moving down - if (yx[1] <= 0 && step[1] < 0) { step[1] *= -1; } - else if (yx[1] >= width - 1 && step[1] > 0) { step[1] *= -1; } + if (yx[1] <= 0 && step[1] < 0) { step[1] *= -1; } // left edge and moving left + else if (yx[1] >= width - 1 && step[1] > 0) { step[1] *= -1; } // right edge and moving right - yx[0] += step[0]; + yx[0] += step[0]; // set ball's next location yx[1] += step[1]; - algo.ball[i] = yx; - algo.direction[i] = step; + algo.ball[i] = yx; // update location + algo.direction[i] = step; // and direction / speed } return map; }; algo.rgbMapStepCount = function (width, height) { + // This make no difference to the script ;-) return 2; }; diff --git a/resources/rgbscripts/blinder.js b/resources/rgbscripts/blinder.js index f1d2008020..8215f32042 100644 --- a/resources/rgbscripts/blinder.js +++ b/resources/rgbscripts/blinder.js @@ -32,14 +32,14 @@ var testAlgo; algo.divisor = 1; algo.properties.push("name:divisor|type:range|display:Divisor|values:1,30|write:setDivisor|read:getDivisor"); - + // Prepare internal properties algo.size = 0; algo.width = 0; algo.height = 0; algo.numX = 0; algo.numY = 0; - + var util = new Object; algo.initialized = false; @@ -54,6 +54,27 @@ var testAlgo; return algo.divisor; }; + // Combine RGB color from color channels + function mergeRgb(r, g, b) { + r = Math.min(255, Math.round(r)); + g = Math.min(255, Math.round(g)); + b = Math.min(255, Math.round(b)); + return ((r << 16) + (g << 8) + b); + } + + function getColor(r, g, b, mRgb) { + // split rgb in to components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + // add the color to the mapped location + pointr += r; + pointg += g; + pointb += b; + // set mapped point + return mergeRgb(pointr, pointg, pointb); + } + util.initialize = function(width, height) { algo.width = width; @@ -67,13 +88,14 @@ var testAlgo; algo.numX = Math.floor(width / algo.size); algo.numY = algo.divisor; } - + algo.bulb = new Array(); var count = 0; for (var w = 0; w < algo.numX; w++) { for (var h = 0; h < algo.numY; h++) { algo.bulb[count] = { + // width / algo.numX * w + width / algo.numX / 2 x: (2 * w + 1 ) * width / 2 / algo.numX - 0.5, y: (2 * h + 1 ) * height / 2 / algo.numY - 0.5, }; @@ -95,17 +117,28 @@ var testAlgo; util.initialize(width, height); } - var map = HSVUtil.createMap(width, height); + // Clear map data + var map = new Array(height); + for (var y = 0; y < height; y++) { + map[y] = new Array(); + for (var x = 0; x < width; x++) { + map[y][x] = 0; + } + } + + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; - var baseH = algo.colors[0].h; - var baseS = algo.colors[0].s; - var baseV = algo.colors[0].v; - var stepPercent = progstep / (algo.rgbMapStepCount(width, height) - 1); var bgPower = Math.pow(stepPercent + 0.1, 2); var bgFactor = Math.min(1, bgPower); - var bgV = baseV * bgFactor; + + // Calculate color pixel related to their positions + var bgPointr = r * bgFactor; + var bgPointg = g * bgFactor; + var bgPointb = b * bgFactor; // for each bulb displayed for (var i = 0; i < algo.bulb.length; i++) { @@ -114,51 +147,56 @@ var testAlgo; for (var rx = algo.bulb[i].xMin; rx <= algo.bulb[i].xMax; rx++) { // Draw only if edges are on the map if (rx >= 0 && rx < width && ry >= 0 && ry < height) { + // Draw the box for debugging. + //map[ry][rx] = getColor(40, 40, 40, map[ry][rx]); + + // calculate the offset difference of map location to the float + // location of the tree var offx = Math.abs((rx - algo.bulb[i].x) / (algo.size / 2)); var offy = Math.abs((ry - algo.bulb[i].y) / (algo.size / 2)); + // Try at: www.geogebra.org/3d + // f(x,y) = 4 sin(π/2 * sqrt(1 - x^2 - y^2)) + // + 6 cos(π/2 * sqrt(1 - x^2 - y^2)) + // - sqrt((4 * x)^2 + (4 * y)^2) + // - 3 + // + relative step + var s = 1 - offx * offx - offy * offy; if (s > 0) { var aFactor = 4 * Math.sin(Math.PI / 2 * Math.sqrt(1 - offx * offx - offy * offy)); var bFactor = 6 * Math.cos(Math.PI / 2 * Math.sqrt(1 - offx * offx - offy * offy)); var cFactor = Math.sqrt(4 * offx * 4 * offx + 4 * offy * 4 * offy); - + var factor = aFactor + bFactor - cFactor - 3 + stepPercent; - + if (factor < 0) { factor = 0; } - // Additive blend on V channel - var idx = (ry * width + rx) * 3; - var newV = baseV * Math.max(0, factor); - if (newV > 0) { - var existV = map[idx + 2]; - if (existV <= 0) { - map[idx] = baseH; - map[idx + 1] = baseS; - } - map[idx + 2] = Math.min(1, existV + newV); - } + // Calculate color pixel related to their positions + var pointr = r * factor; + var pointg = g * factor; + var pointb = b * factor; + + // add the bulb color to the mapped location + map[ry][rx] = getColor(pointr, pointg, pointb, map[ry][rx]); } } } } } - // Apply background: take max of background V and existing V + // Apply the backgruond color. for (var ry = 0; ry < height; ry++) { for (var rx = 0; rx < width; rx++) { - var idx = (ry * width + rx) * 3; - var existV = map[idx + 2]; - if (bgV > existV) { - map[idx] = baseH; - map[idx + 1] = baseS; - map[idx + 2] = bgV; - } + r = Math.max(bgPointr, (map[ry][rx] >> 16) & 0x00FF); + g = Math.max(bgPointg, (map[ry][rx] >> 8) & 0x00FF); + b = Math.max(bgPointb, map[ry][rx] & 0x00FF); + map[ry][rx] = getColor(r, g, b, 0); } } diff --git a/resources/rgbscripts/circles.js b/resources/rgbscripts/circles.js index f395eaa687..8ef881d99e 100644 --- a/resources/rgbscripts/circles.js +++ b/resources/rgbscripts/circles.js @@ -46,12 +46,12 @@ var testAlgo; var circles = new Array(); - function Circle(x, y, step) + function Circle(x, y, step, rgb) { this.xCenter = x; this.yCenter = y; this.step = step; - this.color = {h: 0, s: 0, v: 0}; + this.rgb = rgb; } algo.setAmount = function(_amount) @@ -108,7 +108,7 @@ var testAlgo; } }; - util.initialize = function(size) + util.initialize = function(size, rgb) { if (size > 0) { util.circlesMaxSize = size; @@ -116,20 +116,24 @@ var testAlgo; circles = new Array(); for (var i = 0; i < algo.circlesAmount; i++) { - circles[i] = new Circle(-1, -1, 0); + circles[i] = new Circle(-1, -1, 0, rgb); } util.initialized = true; }; - util.getStepColor = function(step, color) + util.getStepColor = function(step, rgb) { if (algo.fadeMode === 0) { - return {h: color.h, s: color.s, v: color.v}; + return rgb; } else { + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + var stepCount = Math.floor(util.circlesMaxSize / 2); var fadeStep = step; if ( algo.fadeMode === 2 ) { @@ -139,8 +143,12 @@ var testAlgo; } else if (algo.fadeMode == 3 && step < stepCount/2) { fadeStep = step + (stepCount/2) -1; } - var factor = fadeStep / stepCount; - return {h: color.h, s: color.s, v: color.v * factor}; + var newR = Math.round((r / stepCount) * fadeStep); + var newG = Math.round((g / stepCount) * fadeStep); + var newB = Math.round((b / stepCount) * fadeStep); + var newRGB = (newR << 16) + (newG << 8) + newB; + //console.log("mode" + fadeMode + " rgb: " + r + g + b + " nrgb: " + newR+newG+newB +" StepCount: " + stepCount + " step:" + step + " fadeStep:" + fadeStep ) + return newRGB; } }; @@ -164,30 +172,36 @@ var testAlgo; util.drawPixel = function(cx, cy, color, width, height) { + //cx = cx.toFixed(0); + //cy = cy.toFixed(0); cx = Math.round(cx); cy = Math.round(cy); if (cx >= 0 && cx < width && cy >= 0 && cy < height) { - var i = (cy * width + cx) * 3; - util.pixelMap[i] = color.h; - util.pixelMap[i + 1] = color.s; - util.pixelMap[i + 2] = color.v; + util.pixelMap[cy][cx] = color; } }; - util.getNextStep = function(width, height) + util.getNextStep = function(width, height, rgb) { var x = 0; var y = 0; - // create an empty pixelMap (Float32Array, row-major, 3 floats per pixel) - util.pixelMap = HSVUtil.createMap(width, height); + // create an empty, black pixelMap + util.pixelMap = new Array(height); + for (y = 0; y < height; y++) + { + util.pixelMap[y] = new Array(width); + for (x = 0; x < width; x++) { + util.pixelMap[y][x] = 0; + } + } for (var i = 0; i < algo.circlesAmount; i++) { if (circles[i].xCenter === -1) { - circles[i].color = {h: algo.colors[0].h, s: algo.colors[0].s, v: algo.colors[0].v}; + circles[i].rgb = rgb; } - var color = util.getStepColor(circles[i].step, circles[i].color); + var color = util.getStepColor(circles[i].step, circles[i].rgb); //alert("Circle " + i + " xCenter: " + circles[i].xCenter + " color: " + color.toString(16)); if (circles[i].xCenter === -1) { @@ -195,10 +209,7 @@ var testAlgo; if (seed > 50) { continue; } circles[i].xCenter = Math.floor(Math.random() * width); circles[i].yCenter = Math.floor(Math.random() * height); - var idx = (circles[i].yCenter * width + circles[i].xCenter) * 3; - util.pixelMap[idx] = color.h; - util.pixelMap[idx + 1] = color.s; - util.pixelMap[idx + 2] = color.v; + util.pixelMap[circles[i].yCenter][circles[i].xCenter] = color; } else { @@ -243,15 +254,15 @@ var testAlgo; if (util.initialized === false) { if ( algo.circlesSize > 0 ) { - util.initialize(algo.circlesSize); + util.initialize(algo.circlesSize, rgb); } else if (height < width) { - util.initialize(height); + util.initialize(height, rgb); } else { - util.initialize(width); + util.initialize(width, rgb); } } - return util.getNextStep(width, height); + return util.getNextStep(width, height, rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/circular.js b/resources/rgbscripts/circular.js index 0b310df14a..524e2d9559 100644 --- a/resources/rgbscripts/circular.js +++ b/resources/rgbscripts/circular.js @@ -78,7 +78,7 @@ var testAlgo; } angle += Math.PI / 2; if (offx < 0) { - angle += Math.PI; + angle += Math.PI; } return angle; } @@ -208,8 +208,14 @@ var testAlgo; util.circleRadius = height; } - util.map = HSVUtil.createMap(width, height); - + util.map = new Array(height); + for (var y = 0; y < height; y++) { + util.map[y] = new Array(width); + for (var x = 0; x < width; x ++) { + util.map[y][x] = 0; + } + } + util.stepFade = algo.rgbMapStepCount(width, height) / algo.segmentsCount; util.circleFactor = algo.rgbMapStepCount(width, height) / 3; @@ -222,6 +228,51 @@ var testAlgo; util.initialized = true; }; + // Combine RGB color from color channels + util.mergeRgb = function(r, g, b) { + r = Math.min(255, Math.round(r)); + g = Math.min(255, Math.round(g)); + b = Math.min(255, Math.round(b)); + return ((r << 16) + (g << 8) + b); + } + + util.dimColor = function(mRgb, factor) { + if (mRgb < 1) { + return 0; + } + // split rgb into components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + // add the color to the mapped location + pointr *= factor; + pointg *= factor; + pointb *= factor; + // set mapped point + return util.mergeRgb(pointr, pointg, pointb); + } + + util.getColor = function(r, g, b, mRgb) + { + // Stay within boundaries for the input values (do not overshoot in calculation) + r = Math.max(0, Math.min(255, Math.round(r))); + g = Math.max(0, Math.min(255, Math.round(g))); + b = Math.max(0, Math.min(255, Math.round(b))); + + // split rgb in to components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + + // add the color to the algo.mapped location + pointr += r; + pointg += g; + pointb += b; + + // set algo.mapped point + return util.mergeRgb(pointr, pointg, pointb); + } + // Blind out towards 0 percent util.blindoutPercent = function(percent, sharpness) { @@ -240,12 +291,51 @@ var testAlgo; return factor; } - // Compute the geometric brightness factor for a pixel - util.getPixelFactor = function(ry, rx) + util.step = function(width, height, rgb, step) + { + // clear algo.map data + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x ++) { + util.map[y][x] = 0; + } + } + + util.progstep = step; + util.stepPercent = util.progstep / algo.rgbMapStepCount(util.width, util.height); + if (algo.circularMode === 6) { + util.stepAngle = util.twoPi * util.stepPercent; + } + + if (algo.centerRadius !== 0) { + var offsAngle = util.twoPi * util.stepPercent; + var direction = 1; + if (algo.centerRadius < 0) { + direction = -1; + } + var offsFactor = Math.min(util.width, util.height) * algo.centerRadius / 20; + util.vCenterX = util.centerX + Math.sin(offsAngle) * offsFactor; + util.vCenterY = util.centerY + direction * Math.cos(offsAngle) * offsFactor; + } + + + // Optimize multiple calculations + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + + // Draw the current map + for (ry = 0; ry < height; ry++) { + for (rx = 0; rx < width; rx++) { + util.map[ry][rx] = util.getMapPixelColor(ry, rx, r, g, b); + } + } + } + + util.getMapPixelColor = function(ry, rx, r, g, b) { var factor = 1.0; - // calculate the offset difference of map location to the float + // calculate the offset difference of algo.map location to the float // location of the object var offx = rx - util.vCenterX; var offy = ry - util.vCenterY; @@ -273,6 +363,7 @@ var testAlgo; + util.twoPi * factor - Math.PI); } else if (algo.circularMode === 3) { // Right S-Curve +// angle = util.twoPi / (1 - 0.1 / (algo.getWidth() / 10)); var pRadius = Math.sqrt(offx * offx / algo.getWidth() + offy * offy / algo.getWidth()); var virtualx = Math.sin(angle) * pRadius; var virtualy = offy; @@ -310,7 +401,7 @@ var testAlgo; } else if (algo.circularMode === 7) { // Propellor // Calculate the relative angle to the next propellor blade - var segmentAngle = util.twoPi / algo.segmentsCount; + var segmentAngle = util.twoPi / algo.segmentsCount; //algo.segmentsCount; var barAngle = (pointAngle + stepAngle + segmentAngle / 2) % segmentAngle - segmentAngle / 2; // Calculate the distance to the main angle var barDistance = pointRadius * Math.abs(Math.sin(barAngle)); @@ -327,9 +418,9 @@ var testAlgo; var sidefade1 = Math.atan((algo.getWidth() - virtualx - virtualx) / algo.segmentsCount + 1); var sidefade2 = Math.atan((algo.getWidth() + virtualx + virtualx) / algo.segmentsCount + 1); var endfade = Math.atan(virtualy + 1.5); - + factor = endfade + sidefade1 + sidefade2 - 2; - + var fadeFactor = 0; if (algo.fadeMode === 1) { fadeFactor = algo.divisor * Math.atan(1.5 * (angle / util.twoPi)) - algo.divisor + 1; @@ -337,8 +428,8 @@ var testAlgo; else if (algo.fadeMode === 2) { fadeFactor = algo.divisor * Math.atan(1.5 * (1 - (angle / util.twoPi))) - algo.divisor + 1; } - - factor = Math.max(factor, fadeFactor) + + factor = Math.max(factor, fadeFactor); } if (algo.fillMatrix === 0) { @@ -364,45 +455,9 @@ var testAlgo; } } - // Normalize the factor + // Normalize the factor factor = Math.min(1, Math.max(0, factor)); - return factor; - } - - util.step = function(width, height, step) - { - // create a fresh map each frame (Float32Array, zeroed) - util.map = HSVUtil.createMap(width, height); - - util.progstep = step; - util.stepPercent = util.progstep / algo.rgbMapStepCount(util.width, util.height); - if (algo.circularMode === 6) { - util.stepAngle = util.twoPi * util.stepPercent; - } - - if (algo.centerRadius !== 0) { - var offsAngle = util.twoPi * util.stepPercent; - var direction = 1; - if (algo.centerRadius < 0) { - direction = -1; - } - var offsFactor = Math.min(util.width, util.height) * algo.centerRadius / 20; - util.vCenterX = util.centerX + Math.sin(offsAngle) * offsFactor; - util.vCenterY = util.centerY + direction * Math.cos(offsAngle) * offsFactor; - } - - // Draw the current map - for (ry = 0; ry < height; ry++) { - for (rx = 0; rx < width; rx++) { - var factor = util.getPixelFactor(ry, rx); - if (factor > 0) { - var idx = (ry * width + rx) * 3; - util.map[idx] = algo.colors[0].h; - util.map[idx + 1] = algo.colors[0].s; - util.map[idx + 2] = algo.colors[0].v * factor; - } - } - } + return util.getColor(r * factor, g * factor, b * factor, util.map[ry][rx]); } algo.rgbMap = function(width, height, rgb, step) @@ -411,8 +466,8 @@ var testAlgo; { util.initialize(width, height); } - - util.step(width, height, step); + + util.step(width, height, rgb, step); return util.map; }; diff --git a/resources/rgbscripts/empty.js b/resources/rgbscripts/empty.js index 4383a3ced1..f19b34e9df 100644 --- a/resources/rgbscripts/empty.js +++ b/resources/rgbscripts/empty.js @@ -30,17 +30,60 @@ var testAlgo; algo.acceptColors = 2; algo.properties = new Array(); - algo.rgbMapSetColors = function(rawColors) { }; - algo.rgbMapGetColors = function() { return []; }; + var util = new Object; + util.colorArray = new Array(algo.acceptColors); + /** + * Evaluates the rawColors parameter and returns the idx value + * + * @param idx The index of the color in the array + * @return The requested array color or zero in case of invalid input + */ + util.getRawColor = function (idx) { + var color = 0; + if (Array.isArray(util.colorArray) && util.colorArray.length > idx && util.colorArray[idx]) { + color = util.colorArray[idx]; + } + return color; + } + + /** + * Ingests the colors as received through the parameter + * + * @param rawColors The array of colors + */ + algo.rgbMapSetColors = function(rawColors) + { + if (! Array.isArray(rawColors)) + return; + for (var i = 0; i < algo.acceptColors; i++) { + if (i < rawColors.length) + { + util.colorArray[i] = rawColors[i]; + } else { + util.colorArray[i] = 0; + } + } + } + + /** + * The actual "algorithm" for this RGB script. Produces a map of + * size($width, $height) each time it is called. + * + * @param width The width of the matrix in pixels + * @param height The height of the matrix in pixels + * @param rgb Tells the color requested by user in the UI, interpolated between stepCount. + * @param step The step number that is requested (0 to (algo.rgbMapStepCount - 1)) + * @return A two-dimensional matrix array[height][width]. + */ algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; // <-- step color goes here } } diff --git a/resources/rgbscripts/evenodd.js b/resources/rgbscripts/evenodd.js index e3e7cbaae4..4868d7f7a2 100644 --- a/resources/rgbscripts/evenodd.js +++ b/resources/rgbscripts/evenodd.js @@ -31,7 +31,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Even/Odd"; algo.author = "Heikki Junnila"; - algo.acceptColors = 2; /** * The actual "algorithm" for this RGB script. Produces a map of @@ -43,15 +42,17 @@ var testAlgo; */ algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); var i = step; for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if ((i % 2) === 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } i++; } diff --git a/resources/rgbscripts/fill.js b/resources/rgbscripts/fill.js index e349aedfc4..be407f1c95 100644 --- a/resources/rgbscripts/fill.js +++ b/resources/rgbscripts/fill.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Fill"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -51,17 +50,19 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if ((algo.orientation === 0 && x <= step) || (algo.orientation === 1 && y <= step) || (algo.orientation === 2 && x >= width - step - 1) || (algo.orientation === 3 && y >= height - step - 1)) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/fillfromcenter.js b/resources/rgbscripts/fillfromcenter.js index ad59f104c2..aed86e3318 100644 --- a/resources/rgbscripts/fillfromcenter.js +++ b/resources/rgbscripts/fillfromcenter.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Fill From Center"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -55,10 +54,10 @@ var testAlgo; isEven = (height % 2 === 0); } - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { var fill = 0; @@ -76,7 +75,9 @@ var testAlgo; } if(fill === 1) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/fillunfill.js b/resources/rgbscripts/fillunfill.js index 07d9121e87..f87ff22e2f 100644 --- a/resources/rgbscripts/fillunfill.js +++ b/resources/rgbscripts/fillunfill.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Fill Unfill"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -47,26 +46,49 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { - var lit = false; if (algo.orientation === 1) { - if (step < height) { lit = (y <= step); } - else { lit = (y > step - height); } + if (step < height) + { + if (y <= step) { + map[y][x] = rgb; + } else { + map[y][x] = 0; + } + } + else + { + if (y > step - height) { + map[y][x] = rgb; + } else { + map[y][x] = 0; + } + } } else { - if (step < width) { lit = (x <= step); } - else { lit = (x > step - width); } - } - - if (lit) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + if (step < width) + { + if (x <= step) { + map[y][x] = rgb; + } else { + map[y][x] = 0; + } + } + else + { + if (x > step - width) { + map[y][x] = rgb; + } else { + map[y][x] = 0; + } + } } } } diff --git a/resources/rgbscripts/fillunfillfromcenter.js b/resources/rgbscripts/fillunfillfromcenter.js index a2bc612c67..4266f53833 100644 --- a/resources/rgbscripts/fillunfillfromcenter.js +++ b/resources/rgbscripts/fillunfillfromcenter.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Fill Unfill From Center"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -61,10 +60,10 @@ var testAlgo; } var centerStep = center + 1; - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { var cmpAxis = x; @@ -74,14 +73,18 @@ var testAlgo; if (step < centerStep) { if (cmpAxis <= center + step + (isEven ? 1 : 0 ) && cmpAxis >= center - step) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } else { var step2 = step - centerStep; - if (!(cmpAxis <= center + step2 + (isEven ? 1 : 0 ) && cmpAxis >= center - step2)) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + if (cmpAxis <= center + step2 + (isEven ? 1 : 0 ) && cmpAxis >= center - step2) { + map[y][x] = 0; + } else { + map[y][x] = rgb; } } } diff --git a/resources/rgbscripts/fillunfillsquaresfromcenter.js b/resources/rgbscripts/fillunfillsquaresfromcenter.js index 25ff559ad5..91a57badcd 100644 --- a/resources/rgbscripts/fillunfillsquaresfromcenter.js +++ b/resources/rgbscripts/fillunfillsquaresfromcenter.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Fill Unfill Squares From Center"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.rgbMap = function(width, height, rgb, step) { @@ -37,25 +36,29 @@ var testAlgo; var isHeightEven = (height % 2 === 0); var centerStep = (widthCenter > heightCenter ? widthCenter : heightCenter) + 1; - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if (step < centerStep) { if ((x <= widthCenter + step + (isWidthEven ? 1 : 0 ) && x >= widthCenter - step) && (y <= heightCenter + step + (isHeightEven ? 1 : 0) && y >= heightCenter - step)) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } else { var step2 = step - centerStep; - if (!((x <= widthCenter + step2 + (isWidthEven ? 1 : 0 ) && x >= widthCenter - step2) && - (y <= heightCenter + step2 + (isHeightEven ? 1 : 0) && y >= heightCenter - step2))) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + if ((x <= widthCenter + step2 + (isWidthEven ? 1 : 0 ) && x >= widthCenter - step2) && + (y <= heightCenter + step2 + (isHeightEven ? 1 : 0) && y >= heightCenter - step2)) { + map[y][x] = 0; + } else { + map[y][x] = rgb; } } } diff --git a/resources/rgbscripts/fireworks.js b/resources/rgbscripts/fireworks.js index 68123f3eed..fcc4f66ad4 100644 --- a/resources/rgbscripts/fireworks.js +++ b/resources/rgbscripts/fireworks.js @@ -129,31 +129,19 @@ var testAlgo; return algo.particleSteps; }; - function addPixel(map, idx, h, s, v) { - v = Math.max(0, Math.min(1, v)); - if (v <= 0) return; - var existV = map[idx + 2]; - if (existV <= 0) { - map[idx] = h; map[idx + 1] = s; map[idx + 2] = v; - } else { - if (v > existV) { map[idx] = h; map[idx + 1] = s; } - map[idx + 2] = Math.min(1, existV + v); - } - } algo.rgbMap = function(width, height, rgb, progstep) { if (algo.initialized === false || - util.mapWidth != width || - util.mapHeight != height) { + util.map.length != height || + util.map[0].length != width) { util.initialize(width, height); } - // Dim current map data (decay V channel) + // Dim current map data for (var y = 0; y < height; y++) { for (var x = 0; x < width; x ++) { - var idx = (y * width + x) * 3; - util.map[idx + 2] *= 0.8; + util.map[y][x] = util.dimColor(util.map[y][x], 0.8); } } @@ -167,7 +155,7 @@ var testAlgo; // Initialize the rocket if (!algo.rockets[i].initialized || algo.rockets[i].particle.length < algo.particleCount) { - util.initializeRocket(i, width, height); + util.initializeRocket(i, width, height, rgb); } // Trigger the rocket @@ -192,7 +180,7 @@ var testAlgo; // Draw the rocket util.drawObject(algo.rockets[i].x, algo.rockets[i].y, width, height, - algo.rockets[i].h, algo.rockets[i].s, algo.rockets[i].v); + algo.rockets[i].r, algo.rockets[i].g, algo.rockets[i].b); } else { // Countdown particle steps algo.rockets[i].particleSteps--; @@ -206,7 +194,7 @@ var testAlgo; util.drawObject(algo.rockets[i].particle[j].x, algo.rockets[i].particle[j].y, width, height, - algo.rockets[i].h, algo.rockets[i].s, algo.rockets[i].v); + algo.rockets[i].r, algo.rockets[i].g, algo.rockets[i].b); } } @@ -235,10 +223,50 @@ var testAlgo; // random position function for new rocket util.getNewNumberRange = function(minVal, maxVal) { + // Search in the range of min to max + 1 + // which will be reduced by random() excluding 1 + // and floor() reducing to lower number. return Math.floor(Math.random() * (maxVal + 1 - minVal)) + minVal; } - util.drawObject = function(x, y, w, h, oh, os, ov) { + // Combine RGB color from color channels + util.mergeRgb = function(r, g, b) { + r = Math.min(255, Math.round(r)); + g = Math.min(255, Math.round(g)); + b = Math.min(255, Math.round(b)); + return ((r << 16) + (g << 8) + b); + } + + util.getColor = function(r, g, b, mRgb) { + // split rgb in to components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + // add the color to the mapped location + pointr += r; + pointg += g; + pointb += b; + // set mapped point + return util.mergeRgb(pointr, pointg, pointb); + } + + util.dimColor = function(mRgb, factor) { + if (mRgb < 1) { + return 0; + } + // split rgb into components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + // add the color to the mapped location + pointr *= factor; + pointg *= factor; + pointb *= factor; + // set mapped point + return util.mergeRgb(pointr, pointg, pointb); + } + + util.drawObject = function(x, y, w, h, r, g, b) { // workout closest map location for rocket var mx = Math.floor(x); var my = Math.floor(y); @@ -246,18 +274,21 @@ var testAlgo; for (var rx = mx; rx < mx + 2; rx++) { // Draw only if edges are on the map if (rx < w && rx > -1 && ry < h && ry > -1) { + // Draw the box for debugging. + //util.map[ry][rx] = util.getColor(45, 45, 45, 0); var offx = rx - x; var offy = ry - y; var hyp = Math.max(0, 1 - Math.abs(Math.sqrt( (offx * offx) + (offy * offy)))); - var newV = ov * hyp; - var idx = (ry * w + rx) * 3; - addPixel(util.map, idx, oh, os, newV); + var pointr = Math.round(r * hyp); + var pointg = Math.round(g * hyp); + var pointb = Math.round(b * hyp); + util.map[ry][rx] = util.getColor(pointr, pointg, pointb, util.map[ry][rx]); } } } } - util.initializeRocket = function(i, w, h) { + util.initializeRocket = function(i, w, h, rgb) { // reset height and set a start x location algo.rockets[i].x = util.getNewNumberRange(Math.round(w / 5), Math.round(4 * w / 5)); algo.rockets[i].y = h; @@ -275,14 +306,16 @@ var testAlgo; // initialize the rocket color if (algo.randomColor === 0) { do { - algo.rockets[i].h = Math.random(); - algo.rockets[i].s = 0.7 + Math.random() * 0.3; - algo.rockets[i].v = 0.7 + Math.random() * 0.3; - } while (algo.rockets[i].v < 0.5); + // Chose random colour for each rocket + algo.rockets[i].r = Math.round(Math.random() * 255); + algo.rockets[i].g = Math.round(Math.random() * 255); + algo.rockets[i].b = Math.round(Math.random() * 255); + // try again if it is too dim + } while ((algo.rockets[i].r + algo.rockets[i].g + algo.rockets[i].b) < 125); } else { - algo.rockets[i].h = algo.colors[0].h; - algo.rockets[i].s = algo.colors[0].s; - algo.rockets[i].v = algo.colors[0].v; + algo.rockets[i].r = (rgb >> 16) & 0x00FF; + algo.rockets[i].g = (rgb >> 8) & 0x00FF; + algo.rockets[i].b = rgb & 0x00FF; } // initialize particles algo.rockets[i].particle = new Array(); @@ -311,10 +344,14 @@ var testAlgo; algo.rockets[i].initialized = false; } - // Clear map data (Float32Array HSV, row-major) - util.map = HSVUtil.createMap(width, height); - util.mapWidth = width; - util.mapHeight = height; + // Clear map data + util.map = new Array(height); + for (var y = 0; y < height; y++) { + util.map[y] = new Array(); + for (var x = 0; x < width; x++) { + util.map[y][x] = 0; + } + } for (var i = 0; i < algo.rocketsCount; i++) { algo.rockets[i] = { @@ -324,9 +361,9 @@ var testAlgo; yDirection: height, triggerPoint: 0, triggered: false, - h: 0, - s: 0, - v: 0, + r: 0, + g: 0, + b: 0, }; } diff --git a/resources/rgbscripts/flyingobjects.js b/resources/rgbscripts/flyingobjects.js index 7ee4f7fa6f..2d1d7aabd1 100644 --- a/resources/rgbscripts/flyingobjects.js +++ b/resources/rgbscripts/flyingobjects.js @@ -37,8 +37,10 @@ var testAlgo; // Algorithms ---------------------------- var ballAlgo = new Object; - ballAlgo.drawPixel = function(i, rx, ry, h, s, v) + ballAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt((offx * offx) + (offy * offy)); @@ -46,8 +48,7 @@ var testAlgo; if (factor < 0) { factor = 0; } - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var bellAlgo = new Object; @@ -63,35 +64,39 @@ var testAlgo; bellAlgo.cache.clapperDeflection = 0.025 * algo.twoPi; bellAlgo.cache.presetRadius = algo.presetRadius; } - bellAlgo.drawPixel = function(i, rx, ry, h, s, v) + bellAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (bellAlgo.cache.presetRadius != algo.presetRadius) { bellAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt((offx * offx) + (offy * offy)); var factor = 0; + // Offset to bottom var realOffy = offy + bellAlgo.cache.bottomOffset; var scaling = bellAlgo.cache.scaling; factor = 1 - Math.sqrt(offx * offx * scaling + realOffy * realOffy) + realOffy; factor *= util.blindoutPercent(1 - Math.abs(realOffy) / (algo.presetRadius * 1.9), 10); if (offy > bellAlgo.cache.clapperSize) { - var realOffx = rx - algo.obj[i].x; - var stepInput = bellAlgo.cache.clapperDeflection; - stepInput *= algo.progstep; - stepInput += algo.twoPi * algo.obj[i].random; - var stepPercent = Math.sin(stepInput); - realOffx += bellAlgo.cache.clapperSwing * stepPercent; + // The bottom + var realOffx = rx - algo.obj[i].x; + var stepInput = bellAlgo.cache.clapperDeflection; + stepInput *= algo.progstep; + stepInput += algo.twoPi * algo.obj[i].random; + var stepPercent = Math.sin(stepInput); + realOffx += bellAlgo.cache.clapperSwing * stepPercent; realOffy = offy - bellAlgo.cache.clapperSize; distance = Math.sqrt((realOffx * realOffx) + (realOffy * realOffy)); factor = Math.max(factor, 1 - (distance / (algo.presetRadius / 3 + 1))); } - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + // add the object color to the algo.mapped location + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); }; var candleAlgo = new Object; @@ -106,11 +111,13 @@ var testAlgo; candleAlgo.cache.sharpness = 1 / algo.presetRadius; candleAlgo.cache.presetRadius = algo.presetRadius; } - candleAlgo.drawPixel = function(i, rx, ry, h, s, v) + candleAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (candleAlgo.cache.presetRadius != algo.presetRadius) { candleAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt((offx * offx) + (offy * offy)); @@ -130,35 +137,43 @@ var testAlgo; var factor = util.blindoutPercent(1 - distPercent, candleAlgo.cache.sharpness); + // Blindout the bottom realOffy = offy + algo.presetRadius; factor *= util.blindoutPercent(1 - Math.abs(realOffy) / candleAlgo.cache.blindoutPercent, 10); if (offy > candleAlgo.cache.bottomHeight) { + // The bottom: + // offx remains the same. + // offy is 0 distance = Math.sqrt((offx * offx) + 1); factor = Math.max(factor, 1 - (distance / candleAlgo.cache.bottomWidth)); } - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + // add the object color to the algo.mapped location + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var circleAlgo = new Object; - circleAlgo.drawPixel = function(i, rx, ry, h, s, v) + circleAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); var distPercent = distance / algo.presetRadius; + // circle var factor = util.blindoutPercent(1 - distPercent, 0.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var diamondAlgo = new Object; - diamondAlgo.drawPixel = function(i, rx, ry, h, s, v) + diamondAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var percentX = offx / algo.presetRadius; @@ -169,13 +184,14 @@ var testAlgo; + Math.sqrt(percentY * percentY / saturation); factor = 1 - Math.max(0, Math.min(1, factor)); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var diskAlgo = new Object; - diskAlgo.drawPixel = function(i, rx, ry, h, s, v) + diskAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -185,10 +201,13 @@ var testAlgo; angle -= (algo.twoPi) * ((algo.progstep / 64) % 1); angle = (angle + algo.twoPi) % (algo.twoPi); + // Rotating shadow var factor = 0.5 * (Math.abs(Math.cos(angle)) + 1); + // circle factor *= util.blindoutPercent(1 - distPercent, 0.5); + // inner hole var inner = 0.2; if (algo.presetSize < 7) { inner = 0.5; @@ -196,8 +215,7 @@ var testAlgo; distPercent = distance / (algo.presetRadius * inner); factor -= util.blindoutPercent(1 - distPercent, 0.3); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var eyeAlgo = new Object; @@ -211,11 +229,13 @@ var testAlgo; eyeAlgo.cache.targetDistanceInner = algo.presetRadius / 8; eyeAlgo.cache.presetRadius = algo.presetRadius; } - eyeAlgo.drawPixel = function(i, rx, ry, h, s, v) + eyeAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (eyeAlgo.cache.presetRadius != algo.presetRadius) { eyeAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -230,15 +250,14 @@ var testAlgo; offx = rx - turn * algo.obj[i].xDirection - algo.obj[i].x; offy = ry - turn * algo.obj[i].yDirection- algo.obj[i].y; distance = Math.sqrt(offx * offx + offy * offy); - + distPercent = distance / (eyeAlgo.cache.targetDistanceOuter); factor -= util.blindoutPercent(1 - distPercent, 0.5); distPercent = distance / (eyeAlgo.cache.targetDistanceInner); factor += util.blindoutPercent(1 - distPercent, 0.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var flowerAlgo = new Object; @@ -250,11 +269,13 @@ var testAlgo; flowerAlgo.cache.innerCircle = 0.3 * algo.presetRadius; flowerAlgo.cache.presetRadius = algo.presetRadius; } - flowerAlgo.drawPixel = function(i, rx, ry, h, s, v) + flowerAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (flowerAlgo.cache.presetRadius != algo.presetRadius) { flowerAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -269,7 +290,9 @@ var testAlgo; leafs = 4; } + // Turn each object by a random angle angle += algo.obj[i].random * algo.twoPi; + // Repeat and normalize the pattern angle = angle * leafs; angle = (angle + algo.twoPi) % (algo.twoPi); @@ -278,10 +301,10 @@ var testAlgo; factor = util.blindoutPercent(1 - percent, 0.4); factor = (baseIntensity + (1 - baseIntensity) * distPercent) * factor; + // Draw a center factor = Math.max(factor, util.blindoutPercent(1 - distPercentInner, 3)); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var heartAlgo = new Object; @@ -294,11 +317,13 @@ var testAlgo; heartAlgo.cache.circleOffset = algo.presetSize / 5; heartAlgo.cache.presetRadius = algo.presetRadius; } - heartAlgo.drawPixel = function(i, rx, ry, h, s, v) + heartAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (heartAlgo.cache.presetRadius != algo.presetRadius) { heartAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object // top left var offx = rx + heartAlgo.cache.circleOffset - algo.obj[i].x; var offy = ry + algo.halfRadius - algo.obj[i].y; @@ -318,21 +343,22 @@ var testAlgo; // triangle offx = rx - algo.obj[i].x; offy = ry - algo.obj[i].y; - var tips = 3; + var tips = 3; distance = Math.sqrt(offx * offx + offy * offy); angle = geometryCalc.getAngle(offx, offy); targetDistance = geometryCalc.getTargetDistance(angle, tips); distPercent = distance / targetDistance; factor = Math.max(factor, util.blindoutPercent(1 - distPercent, 2)); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var hexagonAlgo = new Object; - hexagonAlgo.drawPixel = function(i, rx, ry, h, s, v) + hexagonAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var tips = 6; + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -341,8 +367,7 @@ var testAlgo; var distPercent = distance / targetDistance; var factor = util.blindoutPercent(1 - distPercent, 1.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var maskAlgo = new Object; @@ -355,11 +380,13 @@ var testAlgo; maskAlgo.cache.openingOffset = algo.presetSize / 5; maskAlgo.cache.presetRadius = algo.presetRadius; } - maskAlgo.drawPixel = function(i, rx, ry, h, s, v) + maskAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (maskAlgo.cache.presetRadius != algo.presetRadius) { maskAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -381,14 +408,15 @@ var testAlgo; distPercent = distance / maskAlgo.cache.targetDistanceOpening; factor -= util.blindoutPercent(1 - distPercent, 3); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var pentagonAlgo = new Object; - pentagonAlgo.drawPixel = function(i, rx, ry, h, s, v) + pentagonAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var tips = 5; + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -397,13 +425,14 @@ var testAlgo; var distPercent = distance / targetDistance; var factor = util.blindoutPercent(1 - distPercent, 1.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var ringAlgo = new Object; - ringAlgo.drawPixel = function(i, rx, ry, h, s, v) + ringAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -418,8 +447,7 @@ var testAlgo; distPercent = distance / (algo.presetRadius * inner); factor -= util.blindoutPercent(1 - distPercent, 0.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var snowflakeAlgo = new Object; @@ -434,19 +462,25 @@ var testAlgo; snowflakeAlgo.cache.cWidthMain = 1.5 * algo.presetRadius; snowflakeAlgo.cache.presetRadius = algo.presetRadius; } - snowflakeAlgo.drawPixel = function(i, rx, ry, h, s, v) + snowflakeAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (snowflakeAlgo.cache.presetRadius != algo.presetRadius) { snowflakeAlgo.updateCache(); } var lines = 3; + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); var factor = 0; + // anti kathete var a = 0; + // kathete var c = 0; + // line width var aWidth = snowflakeAlgo.cache.lineWidth; + // line length var cWidth = 0; var intersect = snowflakeAlgo.cache.intersect; @@ -489,8 +523,7 @@ var testAlgo; c = c / cWidth; factor = Math.max(factor, 1 - (a * a) + 1 - (c * c) - 1); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var snowmanAlgo = new Object; @@ -507,13 +540,15 @@ var testAlgo; snowmanAlgo.cache.yOffset3 = 0.75 * snowmanAlgo.cache.size3 - algo.presetRadius; snowmanAlgo.cache.presetRadius = algo.presetRadius; } - snowmanAlgo.drawPixel = function(i, rx, ry, h, s, v) + snowmanAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (snowmanAlgo.cache.presetRadius != algo.presetRadius) { snowmanAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; - + // Ball 1 var offy = ry - algo.obj[i].y + snowmanAlgo.cache.yOffset1; var factor1 = Math.max(0, Math.min(1, 1 - (Math.sqrt((offx * offx) + (1.5 * offy * offy)) / (snowmanAlgo.cache.size1 + 1)))); @@ -526,17 +561,19 @@ var testAlgo; offy = ry - algo.obj[i].y + snowmanAlgo.cache.yOffset3; var factor3 = Math.max(0, Math.min(1, 1 - (Math.sqrt((offx * offx) + (1.5 * offy * offy)) / (snowmanAlgo.cache.size3 + 1)))); + // Merge the balls var factor = Math.max(factor1, factor2); factor = Math.max(factor, factor3); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var squareAlgo = new Object; - squareAlgo.drawPixel = function(i, rx, ry, h, s, v) + squareAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var tips = 4; + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -549,8 +586,7 @@ var testAlgo; var distPercent = distance / targetDistance; var factor = util.blindoutPercent(1 - distPercent, 2.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var starAlgo = new Object; @@ -564,11 +600,13 @@ var testAlgo; starAlgo.cache.sharpness = 0.3 + algo.presetSize / 18; starAlgo.cache.presetRadius = algo.presetRadius; } - starAlgo.drawPixel = function(i, rx, ry, h, s, v) + starAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (starAlgo.cache.presetRadius != algo.presetRadius) { starAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var baseIntensity = 0.3; var tips = 5; var offx = rx - algo.obj[i].x; @@ -589,10 +627,12 @@ var testAlgo; c = c / cWidth; factor = Math.max(0, 1 - (a * a) + 1 - (c * c) - 1); } else { + // Repeat and normalize the pattern angle = geometryCalc.getAngle(offx, offy) + Math.PI; var colorAngle = tips * angle; colorAngle = (colorAngle + (Math.PI)) % (algo.twoPi); + // Calculate color pixel positions, base color var distPercent = distance / algo.presetRadius; factor = baseIntensity + (1 - baseIntensity) * (Math.abs(colorAngle - Math.PI) / algo.twoPi @@ -608,8 +648,7 @@ var testAlgo; factor = factor * util.blindoutPercent(1 - distPercent, starAlgo.cache.sharpness); } - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var steeringwheelAlgo = new Object; @@ -626,15 +665,19 @@ var testAlgo; steeringwheelAlgo.cache.innerCircle = 0.65 * algo.presetRadius; } steeringwheelAlgo.cache.centerCircle = 0.2 * algo.presetRadius; + // line width steeringwheelAlgo.cache.lineWidth = 0.9 + 0.06 * algo.presetRadius; + // line length steeringwheelAlgo.cache.lineLength = 1.4 * algo.presetRadius; steeringwheelAlgo.cache.presetRadius = algo.presetRadius; } - steeringwheelAlgo.drawPixel = function(i, rx, ry, h, s, v) + steeringwheelAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (steeringwheelAlgo.cache.presetRadius != algo.presetRadius) { steeringwheelAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -642,6 +685,7 @@ var testAlgo; var lines = 3; var factor = 0; + // Draw handles var angle = geometryCalc.getAngle(offx, offy) * lines; var a = Math.abs(Math.sin(angle) * distance); a = a / steeringwheelAlgo.cache.lineWidth / lines; @@ -649,16 +693,17 @@ var testAlgo; c = c / steeringwheelAlgo.cache.lineLength; factor = 1 - (a * a) - (c * c); + // Draw a ring var distPercent = distance / steeringwheelAlgo.cache.outerCircle; var ringFactor = Math.max(factor, util.blindoutPercent(1 - distPercent, 3)); distPercent = distance / steeringwheelAlgo.cache.innerCircle; ringFactor -= util.blindoutPercent(1 - distPercent, 5); factor = Math.max(factor, ringFactor); + // Draw a center factor = Math.max(factor, util.blindoutPercent(1 - distPercentCenter, 3)); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var tornadoAlgo = new Object; @@ -670,38 +715,46 @@ var testAlgo; tornadoAlgo.cache.centerCircle = 0.15 * algo.presetRadius; tornadoAlgo.cache.presetRadius = algo.presetRadius; } - tornadoAlgo.drawPixel = function(i, rx, ry, h, s, v) + tornadoAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (tornadoAlgo.cache.presetRadius != algo.presetRadius) { tornadoAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); var angle = geometryCalc.getAngle(offx, offy); var percent = 0; var factor = 0; - + + // Repeat the pattern var fanblades = 3; if (algo.presetSize >= 9) { fanblades = 5; } - + angle -= (distance / (algo.presetRadius)) * algo.halfPi; + // Repeat the pattern angle = fanblades * angle; + // Turn along the distance angle += (algo.twoPi) * (algo.progstep / 8 % 1); + // Normalize the angle angle = (angle + algo.twoPi) % (algo.twoPi); + // Blind along the edges factor = Math.cos(angle); + // Blind out the outside edges percent = Math.max(0, 1 - (distance / algo.presetRadius)); factor *= util.blindoutPercent(percent, 1) - + + // Draw a center var distPercentCenter = distance / tornadoAlgo.cache.centerCircle; factor = Math.max(factor, util.blindoutPercent(1 - distPercentCenter, 3)); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var treeAlgo = new Object; @@ -717,11 +770,13 @@ var testAlgo; treeAlgo.cache.sharpness = 0.025 * algo.presetRadius; treeAlgo.cache.presetRadius = algo.presetRadius; } - treeAlgo.drawPixel = function(i, rx, ry, h, s, v) + treeAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { if (treeAlgo.cache.presetRadius != algo.presetRadius) { treeAlgo.updateCache(); } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var factor = 0; @@ -739,43 +794,52 @@ var testAlgo; var distPercent = realDistance / targetDistance; factor = util.blindoutPercent(1 - distPercent, treeAlgo.cache.sharpness); + // blind out the bottom factor *= util.blindoutPercent(1 - Math.abs(offy) / treeAlgo.cache.blindoutPercent, 10); if (offy > treeAlgo.cache.bottomHeight) { + // The bottom realOffy = offy - treeAlgo.cache.bottomHeight; var distance = Math.sqrt((offx * offx) + 1); factor = Math.max(factor, 1 - (distance / treeAlgo.cache.bottomWidth)); } - - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + + // add the object color to the algo.mapped location + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } - treeAlgo.unused = function(i, rx, ry, h, s, v) + treeAlgo.unused = function(i, rx, ry, r, g, b) { + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var factor = 0; + // Calculate color intensity if (ry === Math.floor(algo.obj[i].y) + algo.boxRadius + 1) { + // The tree foot offy = 0; } else { + // Offset to bottom offy += (algo.presetRadius) + 0.7; } factor = ((1 - (Math.sqrt((offx * offx * 1.8) + (offy * offy * 1.4)))) * 2.5) + offy * 3; factor = factor / 3.27; - - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + + // add the object color to the algo.mapped location + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); }; var triangleAlgo = new Object; - triangleAlgo.drawPixel = function(i, rx, ry, h, s, v) + triangleAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var tips = 3; var rotation = algo.twoPi * algo.obj[i].random; if (algo.presetSize < 10) { rotation = Math.PI; } + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); @@ -784,22 +848,23 @@ var testAlgo; var distPercent = distance / targetDistance; var factor = util.blindoutPercent(1 - distPercent, 3); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var ufoAlgo = new Object; - ufoAlgo.drawPixel = function(i, rx, ry, h, s, v) + ufoAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var offx = rx - algo.obj[i].x; - + var size = algo.presetSize * 2 / 5; var offy1 = ry - algo.obj[i].y; var factor1 = 1 - (Math.sqrt((offx * offx) + (offy1 * offy1)) / ((size / 2) + 1)); + // Set a bit towards background by dimming. if (factor1 < 0) { factor1 = 0; } + // factor1 = 0; var factor2 = 0; var offy2 = ry - algo.obj[i].y - size / 4; @@ -816,21 +881,24 @@ var testAlgo; factor = 1; } - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } var ventilatorAlgo = new Object; - ventilatorAlgo.drawPixel = function(i, rx, ry, h, s, v) + ventilatorAlgo.getMapPixelColor = function(i, rx, ry, r, g, b) { var factor = 1.0; + // calculate the offset difference of algo.map location to the float + // location of the object var offx = rx - algo.obj[i].x; var offy = ry - algo.obj[i].y; var distance = Math.sqrt(offx * offx + offy * offy); var angle = geometryCalc.getAngle(offx, offy); + // Optimize multiple calculations var baseIntensity = 0.1; var percent = 0; + // Repeat the pattern var fanblades = 3; if (algo.presetSize >= 10) { fanblades = 5; @@ -839,21 +907,28 @@ var testAlgo; angle -= (algo.twoPi) * (algo.progstep / 16 % 1); angle = (angle + algo.twoPi) % (algo.twoPi); + // Draw a center cover + // asec consumes values > 1. asec(x) = acos(1/x) percent = Math.max(0, 1 - (distance / (0.2 * algo.presetRadius))); + // apply a scale factor for the percent / input in the asec function var factorC = algo.halfPi * Math.acos(1 / (2.5 * percent + 1)) / (algo.halfPi); + // Calculate intensity by angle factor = Math.max(0, (1.0 + baseIntensity) * angle / (algo.twoPi) - baseIntensity); + // Blind out the inner edges percent = Math.max(0, 1 - (angle / algo.twoPi)); + // apply a scale factor for the percent / input in the asec function factor *= (1.0 - baseIntensity) * (Math.acos(1 / (2.5 * percent + 1)) / (algo.halfPi)) + baseIntensity; + // Blind out the outside edges percent = Math.max(0, 1 - (distance / (algo.presetRadius))); + // apply a scale factor for the percent / input in the asec function factor *= algo.halfPi * Math.acos(1 / (2.5 * percent + 1)) / (algo.halfPi); - + factor = Math.max(factorC, factor * 1.5); - var idx = (ry * algo.mapWidth + rx) * 3; - util.addPixel(idx, h, s, HSVUtil.clamp01(factor) * v); + return util.getColor(r * factor, g * factor, b * factor, algo.map[ry][rx]); } // Algorithm registration and methods ------------------------ @@ -937,6 +1012,7 @@ var testAlgo; algo.realsize = 1; algo.progstep = 0; algo.totalSteps = 1024; + // Optimize multiple calculations algo.twoPi = 2 * Math.PI; algo.halfPi = Math.PI / 2; @@ -1020,7 +1096,7 @@ var testAlgo; 0.866 * algo.presetRadius; } else if (tips === 5) { geometryCalc.cache.innerRadius = - 0.688 * algo.presetRadius / 0.851; + 0.688 * algo.presetRadius / 0.851; } else if (tips === 4) { geometryCalc.cache.innerRadius = algo.presetRadius * 0.707; @@ -1039,7 +1115,7 @@ var testAlgo; geometryCalc.cache.tips != tips) { geometryCalc.updateCache(tips); } - + var anglePart = (angle + geometryCalc.cache.r) % (algo.twoPi / tips) - geometryCalc.cache.r; var targetDistance = geometryCalc.cache.innerRadius / @@ -1052,7 +1128,9 @@ var testAlgo; geometryCalc.getAngle = function(offx, offy) { var angle = 0; + // catch offx == 0 if (offx == 0) { + // This where the asymptote goes if (offy < 0) { angle = -1 * Math.PI / 2; } else { @@ -1064,11 +1142,11 @@ var testAlgo; } angle += Math.PI / 2; if (offx < 0) { - angle += Math.PI; + angle += Math.PI; } return angle; } - + // Utility functions -------------------------- util.initialize = function(width, height) @@ -1077,10 +1155,16 @@ var testAlgo; for (var i = 0; i < algo.presetNumber; i++) { algo.obj[i] = { + // set random start locations for objects x: Math.random() * (width - 1), y: Math.random() * (height - 1), + // General-purpose random number per object random: Math.random(), }; + // For DEVELOPMENT align objects centered. + //algo.obj[i].x = width / 2; + //algo.obj[i].y = height / 2; + // set random directions do { do { algo.obj[i].xDirection = (Math.random() * 2) - 1; @@ -1089,12 +1173,16 @@ var testAlgo; algo.obj[i].yDirection = (Math.random() * 2) - 1; } while (Math.abs(algo.obj[i].yDirection) < 0.1); } while (Math.abs(algo.obj[i].xDirection + algo.obj[i].yDirection) < 0.3); - // Random HSV colour for each object - algo.obj[i].h = Math.random(); - algo.obj[i].s = 0.7 + Math.random() * 0.3; - algo.obj[i].v = 0.7 + Math.random() * 0.3; + do { + // Chose random colour for each object + algo.obj[i].r = Math.round(Math.random() * 255); + algo.obj[i].g = Math.round(Math.random() * 255); + algo.obj[i].b = Math.round(Math.random() * 255); + // try again if it is too dim + } while ((algo.obj[i].r + algo.obj[i].g + algo.obj[i].b) < 125); } + // area size to draw object algo.boxRadius = Math.round(algo.presetRadius); algo.realsize = Math.floor(algo.presetRadius) * 2 + 1; @@ -1102,24 +1190,37 @@ var testAlgo; return; }; - // Additive HSV pixel blending - util.addPixel = function(idx, h, s, v) + // Combine RGB color from color channels + util.mergeRgb = function(r, g, b) { - v = Math.max(0, Math.min(1, v)); - if (v <= 0) return; - var existV = algo.map[idx + 2]; - if (existV <= 0) { - algo.map[idx] = h; - algo.map[idx + 1] = s; - algo.map[idx + 2] = v; - } else { - if (v > existV) { - algo.map[idx] = h; - algo.map[idx + 1] = s; - } - algo.map[idx + 2] = Math.min(1, existV + v); - } - }; + // Stay within boundaries for the final color + r = Math.max(0, Math.min(255, Math.round(r))); + g = Math.max(0, Math.min(255, Math.round(g))); + b = Math.max(0, Math.min(255, Math.round(b))); + + return ((r << 16) + (g << 8) + b); + } + + util.getColor = function(r, g, b, mRgb) + { + // Stay within boundaries for the input values (do not overshoot in calculation) + r = Math.max(0, Math.min(255, Math.round(r))); + g = Math.max(0, Math.min(255, Math.round(g))); + b = Math.max(0, Math.min(255, Math.round(b))); + + // split rgb in to components + var pointr = (mRgb >> 16) & 0x00FF; + var pointg = (mRgb >> 8) & 0x00FF; + var pointb = mRgb & 0x00FF; + + // add the color to the algo.mapped location + pointr += r; + pointg += g; + pointb += b; + + // set algo.mapped point + return util.mergeRgb(pointr, pointg, pointb); + } // Blind out towards 0 percent util.blindoutPercent = function(percent, sharpness) @@ -1130,7 +1231,9 @@ var testAlgo; if (percent < 0) { return 0; } + // Normalize input percent = Math.min(1, percent); + // asec consumes values > 1. asec(x) = acos(1/x) var factor = Math.min(1, Math.acos(1 / (Math.sqrt(sharpness * percent * percent) + 1) ) * algo.halfPi); @@ -1145,39 +1248,59 @@ var testAlgo; } algo.progstep = progstep; - algo.map = HSVUtil.createMap(width, height); - algo.mapWidth = width; - algo.mapHeight = height; + // Clear algo.map data + algo.map = new Array(height); + for (var y = 0; y < height; y++) { + algo.map[y] = new Array(); + for (var x = 0; x < width; x++) { + algo.map[y][x] = 0; + } + } var shape = shapes.getAlgoObject(algo.selectedAlgo); + // for each object displayed for (var i = 0; i < algo.presetNumber; i++) { + // workout closest map location for object var mx = Math.floor(algo.obj[i].x); var my = Math.floor(algo.obj[i].y); - var h = algo.obj[i].h; - var s = algo.obj[i].s; - var v = algo.obj[i].v; + // split colour + var r = algo.obj[i].r; + var g = algo.obj[i].g; + var b = algo.obj[i].b; if (algo.presetRandom != 0) { - h = algo.colors[0].h; - s = algo.colors[0].s; - v = algo.colors[0].v; + r = (rgb >> 16) & 0x00FF; + g = (rgb >> 8) & 0x00FF; + b = rgb & 0x00FF; } for (var ry = my - algo.boxRadius; ry < my + algo.boxRadius + 2; ry++) { for (var rx = mx - algo.boxRadius; rx < mx + algo.boxRadius + 2; rx++) { + // Draw only if edges are on the map if (rx < width && rx > -1 && ry < height && ry > -1) { - shape.drawPixel(i, rx, ry, h, s, v); + // DEVELOPMENT: Draw a box for debugging. + //algo.map[ry][rx] = util.getColor(0, 0, 80, algo.map[ry][rx]); + + // add the object color to the mapped location + algo.map[ry][rx] = shape.getMapPixelColor(i, rx, ry, r, g, b); } } } + // if collision detection is on if (algo.presetCollision === 0) { + // object collision detection + // check all objects for (var ti = 0; ti < algo.presetNumber; ti++) { + // but not the current one if (ti !== i) { + // calculate distance to current object var disx = (algo.obj[i].x + algo.obj[i].xDirection) - algo.obj[ti].x; var disy = (algo.obj[i].y + algo.obj[i].yDirection) - algo.obj[ti].y; var dish = Math.sqrt((disx * disx) + (disy * disy)); + // if to close if (dish < (1.414) * (algo.presetRadius)) { + // swap speed / direction of current object var stepx = algo.obj[i].xDirection; var stepy = algo.obj[i].yDirection; algo.obj[i].xDirection = algo.obj[ti].xDirection; @@ -1189,18 +1312,24 @@ var testAlgo; } } + // edge collision detection if (algo.obj[i].y <= 0 && algo.obj[i].yDirection < 0) { + // top edge and moving up algo.obj[i].yDirection *= -1; } else if (algo.obj[i].y >= height - 1 && algo.obj[i].yDirection > 0) { + // bottom edge and moving down algo.obj[i].yDirection *= -1; } if (algo.obj[i].x <= 0 && algo.obj[i].xDirection < 0) { + // left edge and moving left algo.obj[i].xDirection *= -1; } else if (algo.obj[i].x >= width - 1 && algo.obj[i].xDirection > 0) { + // right edge and moving right algo.obj[i].xDirection *= -1; } + // set object's next location algo.obj[i].x += algo.obj[i].xDirection; algo.obj[i].y += algo.obj[i].yDirection; } @@ -1210,6 +1339,7 @@ var testAlgo; algo.rgbMapStepCount = function(width, height) { + // This make no difference to the script, except for fading the colors return algo.totalSteps; }; diff --git a/resources/rgbscripts/gradient.js b/resources/rgbscripts/gradient.js index 1e712692dd..a73f8d86d9 100644 --- a/resources/rgbscripts/gradient.js +++ b/resources/rgbscripts/gradient.js @@ -40,14 +40,10 @@ var testAlgo; util.initialized = false; util.gradientData = new Array(); util.presets = new Array(); - // Rainbow: Red -> Green -> Blue - util.presets.push([{h: 0, s: 1, v: 1}, {h: 0.3333, s: 1, v: 1}, {h: 0.6667, s: 1, v: 1}]); - // Sunset: Yellow -> Red - util.presets.push([{h: 0.1667, s: 1, v: 1}, {h: 0, s: 1, v: 1}]); - // Abstract: Blue-ish -> Cyan -> Magenta -> Yellow - util.presets.push([{h: 0.639, s: 0.667, v: 1}, {h: 0.5, s: 1, v: 1}, {h: 0.8333, s: 1, v: 1}, {h: 0.1667, s: 1, v: 1}]); - // Ocean: Deep Blue -> Cyan - util.presets.push([{h: 0.614, s: 1, v: 0.725}, {h: 0.514, s: 0.992, v: 1}]); + util.presets.push(new Array(0xFF0000, 0x00FF00, 0x0000FF)); + util.presets.push(new Array(0xFFFF00, 0xFF0000)); + util.presets.push(new Array(0x5571FF, 0x00FFFF, 0xFF00FF, 0xFFFF00)); + util.presets.push(new Array(0x003AB9, 0x02EAFF)); algo.setPreset = function(_preset) { @@ -94,37 +90,38 @@ var testAlgo; else { return "Horizontal"; } }; - // Shortest-arc hue interpolation - function lerpHue(h1, h2, t) { - var diff = h2 - h1; - if (diff > 0.5) { diff -= 1; } - else if (diff < -0.5) { diff += 1; } - var h = h1 + diff * t; - if (h < 0) { h += 1; } - else if (h >= 1) { h -= 1; } - return h; - } - util.initialize = function() { + // calculate the gradient for the selected preset + // with the given width var gradIdx = 0; util.gradientData = new Array(); - var preset = util.presets[algo.presetIndex]; - for (var i = 0; i < preset.length; i++) + for (var i = 0; i < util.presets[algo.presetIndex].length; i++) { - var sColor = preset[i]; - var eColor = preset[(i + 1) % preset.length]; - - util.gradientData[gradIdx++] = {h: sColor.h, s: sColor.s, v: sColor.v}; + var sColor = util.presets[algo.presetIndex][i]; + var eColor = util.presets[algo.presetIndex][i + 1]; + if (eColor == undefined) { + eColor = util.presets[algo.presetIndex][0]; + } + util.gradientData[gradIdx++] = sColor; + var sr = (sColor >> 16) & 0x00FF; + var sg = (sColor >> 8) & 0x00FF; + var sb = sColor & 0x00FF; + var er = (eColor >> 16) & 0x00FF; + var eg = (eColor >> 8) & 0x00FF; + var eb = eColor & 0x00FF; + + var stepR = ((er - sr) / (algo.presetSize)); + var stepG = ((eg - sg) / (algo.presetSize)); + var stepB = ((eb - sb) / (algo.presetSize)); for (var s = 1; s < algo.presetSize; s++) { - var t = s / algo.presetSize; - util.gradientData[gradIdx++] = { - h: lerpHue(sColor.h, eColor.h, t), - s: sColor.s + (eColor.s - sColor.s) * t, - v: sColor.v + (eColor.v - sColor.v) * t - }; + var gradR = Math.floor(sr + (stepR * s)) & 0x00FF; + var gradG = Math.floor(sg + (stepG * s)) & 0x00FF; + var gradB = Math.floor(sb + (stepB * s)) & 0x00FF; + var gradRGB = (gradR << 16) + (gradG << 8) + gradB; + util.gradientData[gradIdx++] = gradRGB; } } util.initialized = true; @@ -138,9 +135,10 @@ var testAlgo; } var gradStep = 0; - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); if (algo.orientation === 1) { gradStep = step + y; @@ -162,9 +160,7 @@ var testAlgo; gradStep = (gradStep % util.gradientData.length); } - var color = util.gradientData[gradStep]; - var i = (y * width + x) * 3; - map[i] = color.h; map[i + 1] = color.s; map[i + 2] = color.v; + map[y][x] = util.gradientData[gradStep]; } } diff --git a/resources/rgbscripts/lines.js b/resources/rgbscripts/lines.js index 92cf69711e..ac59e16092 100644 --- a/resources/rgbscripts/lines.js +++ b/resources/rgbscripts/lines.js @@ -58,7 +58,7 @@ var testAlgo; this.xCenter = x; this.yCenter = y; this.step = step; - this.color = {h: 0, s: 0, v: 0}; + this.color = 0; }; algo.setLinesSize = function(_size) @@ -218,21 +218,30 @@ var testAlgo; util.initialized = true; }; - util.getColor = function(step, color) + util.getColor = function(step, rgb) { if (algo.fadeMode === 0) { - return color; + return rgb; } else { + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + var stepCount = Math.floor(util.linesMaxSize); var fadeStep = step; if (algo.fadeMode === 2) { fadeStep = stepCount - step; } var factor = fadeStep / stepCount; - return {h: color.h, s: color.s, v: color.v * factor}; + var newR = Math.round(r * factor); + var newG = Math.round(g * factor); + var newB = Math.round(b * factor); + + var newRGB = (newR << 16) + (newG << 8) + newB; + return newRGB; } }; @@ -241,10 +250,7 @@ var testAlgo; cx = Math.round(cx); cy = Math.round(cy); if (cx >= 0 && cx < width && cy >= 0 && cy < height) { - var i3 = (cy * width + cx) * 3; - util.pixelMap[i3] = color.h; - util.pixelMap[i3 + 1] = color.s; - util.pixelMap[i3 + 2] = color.v; + util.pixelMap[cy][cx] = color; } }; @@ -252,7 +258,14 @@ var testAlgo; { var x, y; // create an empty, black pixelMap - util.pixelMap = HSVUtil.createMap(width, height); + util.pixelMap = new Array(height); + for (y = 0; y < height; y++) + { + util.pixelMap[y] = new Array(width); + for (x = 0; x < width; x++) { + util.pixelMap[y][x] = 0; + } + } for (var i = 0; i < algo.linesAmount; i++) { @@ -264,7 +277,7 @@ var testAlgo; continue; // apply the current step color - lines[i].color = {h: algo.colors[0].h, s: algo.colors[0].s, v: algo.colors[0].v}; + lines[i].color = rgb; // if biased .. move the start points to the cardinal ends of the space if (algo.linesBias == 1 || algo.linesBias == 5 || algo.linesBias == 6) @@ -293,11 +306,7 @@ var testAlgo; lines[i].xCenter = Math.round(Math.random() * (width - 1)); } - var ic = lines[i].color; - var i3 = (lines[i].yCenter * width + lines[i].xCenter) * 3; - util.pixelMap[i3] = ic.h; - util.pixelMap[i3 + 1] = ic.s; - util.pixelMap[i3 + 2] = ic.v; + util.pixelMap[lines[i].yCenter][lines[i].xCenter] = lines[i].color; } else { var color = util.getColor(lines[i].step, lines[i].color); //alert("Line " + i + " xCenter: " + lines[i].xCenter + " color: " + color.toString(16)); diff --git a/resources/rgbscripts/marquee.js b/resources/rgbscripts/marquee.js index 5f5a3ff92e..0b7d5068b2 100644 --- a/resources/rgbscripts/marquee.js +++ b/resources/rgbscripts/marquee.js @@ -44,8 +44,9 @@ var testAlgo; util.initialized = false; util.width = 0; util.height = 0; - util.featureColor = {h: 0, s: 0, v: 0}; + util.featureColor = 0; util.step = algo.marqueeCount; + util.colorArray = new Array(algo.acceptColors); util.lights = new Array(); util.feature = new Array(); @@ -93,14 +94,32 @@ var testAlgo; return algo.marqueeCount; }; - algo.rgbMapSetColors = function(rawColors) { }; - algo.rgbMapGetColors = function() { return []; }; + util.getRawColor = function (idx) { + var color = 0; + if (Array.isArray(util.colorArray) && util.colorArray.length > idx && util.colorArray[idx]) { + color = util.colorArray[idx]; + } + return color; + } + + algo.rgbMapSetColors = function(rawColors) + { + if (! Array.isArray(rawColors)) + return; + for (var i = 0; i < algo.acceptColors; i++) { + if (i < rawColors.length) + { + util.colorArray[i] = rawColors[i]; + } else { + util.colorArray[i] = 0; + } + } + } util.initialize = function (width, height) { // initialize feature - var fc = algo.colors[0]; - util.featureColor = {h: fc.h, s: fc.s, v: fc.v}; + util.featureColor = util.getRawColor(0); util.feature = new Array(); var maxDistance = Math.min(width, height) / 2; for (var y = 0; y < height; y++) { @@ -111,8 +130,9 @@ var testAlgo; } for (var x = 0; x < width; x++) { + // write color var distance = algo.edgeDepth + 1; - util.feature[y][x] = {h: 0, s: 0, v: 0}; + util.feature[y][x] = 0; var x_distance = x; if (x >= width / 2) { @@ -121,13 +141,16 @@ var testAlgo; distance = Math.min(x_distance, y_distance); if (distance <= algo.edgeDepth && distance <= maxDistance) { - var percent = ((algo.edgeDepth - distance) / algo.edgeDepth); - util.feature[y][x] = {h: util.featureColor.h, s: util.featureColor.s, v: util.featureColor.v * percent}; + var percent = ((algo.edgeDepth - distance) / algo.edgeDepth) * 100; + util.feature[y][x] = util.fadeColor(util.featureColor, percent); + } else { + util.feature[y][x] = 0; } } } // initialize lights array: 2 heights, 2 widths, 4 duplicate corner pixels + // only if the dimensions have changes (not on color change) if (util.width != width || util.height != height || util.initialized !== true) { var length = height * 2 + width * 2 - 4; util.lights = new Array(length); @@ -141,21 +164,31 @@ var testAlgo; } } } + // for testing for change util.width = width; util.height = height; util.initialized = true; }; + util.fadeColor = function (rgb, percent) { + var r = (rgb >> 16) & 0x00ff; + var g = (rgb >> 8) & 0x00ff; + var b = rgb & 0x00ff; + var newR = Math.round(r * (percent / 100)); + var newG = Math.round(g * (percent / 100)); + var newB = Math.round(b * (percent / 100)); + var newRGB = (newR << 16) + (newG << 8) + newB; + return newRGB; + }; + util.getNextStep = function (width, height) { var x = 0; var y = 0; - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (y = 0; y <= height - 1; y++) { + map[y] = new Array(width); for (x = 0; x <= width - 1; x++) { - var c = util.feature[y][x]; - if (c.v > 0) { - HSVUtil.setPixel(map, width, x, y, c.h, c.s, c.v); - } + map[y][x] = util.feature[y][x]; } } @@ -170,13 +203,13 @@ var testAlgo; } // create light map add lights, go around the outside - var mc = algo.colors.length > 1 ? algo.colors[1] : algo.colors[0]; + var marqueeColor = util.getRawColor(1); var p = 0; // left for (y = 0; y < height; y++) { x = 0; if (util.lights[p] === 1) { - HSVUtil.setPixel(map, width, x, y, mc.h, mc.s, mc.v); + map[y][x] = marqueeColor; } p += 1; } @@ -184,7 +217,7 @@ var testAlgo; for (x = 1; x < width; x++) { y = height - 1; if (util.lights[p] === 1) { - HSVUtil.setPixel(map, width, x, y, mc.h, mc.s, mc.v); + map[y][x] = marqueeColor; } p += 1; } @@ -192,7 +225,7 @@ var testAlgo; for (y = height - 2; y >= 0; y--) { x = width - 1; if (util.lights[p] === 1) { - HSVUtil.setPixel(map, width, x, y, mc.h, mc.s, mc.v); + map[y][x] = marqueeColor; } p += 1; } @@ -200,7 +233,7 @@ var testAlgo; for (x = width - 2; x >= 0; x--) { y = 0; if (util.lights[p] === 1) { - HSVUtil.setPixel(map, width, x, y, mc.h, mc.s, mc.v); + map[y][x] = marqueeColor; } p += 1; } @@ -208,12 +241,9 @@ var testAlgo; }; algo.rgbMap = function(width, height, rgb, step) { - var fc = algo.colors[0]; if ( util.initialized === false || - util.featureColor.h !== fc.h || - util.featureColor.s !== fc.s || - util.featureColor.v !== fc.v || + util.featureColor != util.getRawColor(0) || util.width !== width || util.height !== height ) { diff --git a/resources/rgbscripts/noise.js b/resources/rgbscripts/noise.js index 675d0af83b..58d177e6dc 100644 --- a/resources/rgbscripts/noise.js +++ b/resources/rgbscripts/noise.js @@ -42,18 +42,39 @@ var testAlgo; return algo.noisePercentage; }; + // QLC+ rgbMap function where the work is done algo.rgbMap = function (width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = []; + for (var x = 0; x < width; x++) { - // Random brightness factor 0-1, applied to user color's value - var randV = algo.colors[0].v * Math.random(); + var r = (rgb >> 16) & 0x00FF; // split color of user selected color + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + + // create random color level from 1 to 255 + var colorLevel = Math.floor(Math.random() * 255); + + // Assign random color value to temp variables + var rr = colorLevel; + var gg = colorLevel; + var bb = colorLevel; - var vDiv = 0; + // Limit each color element to the maximum for chosen color or make 0 if below 0 + if (rr > r) { rr = r; } + if (rr < 0) { rr = 0; } + if (gg > g) { gg = g; } + if (gg < 0) { gg = 0; } + if (bb > b) { bb = b; } + if (bb < 0) { bb = 0; } + + var cColor = (rr << 16) + (gg << 8) + bb; // put rgb parts back together + var vDiv = 0; // for noise amount use // setup for noise reduction :) switch (algo.noisePercentage) @@ -69,10 +90,13 @@ var testAlgo; break; } - dCounter += 1; - if (dCounter >= vDiv) { - dCounter = 0; - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, randV); + dCounter += 1; // counter for noise trigger + if (dCounter >= vDiv) { // compare counter to user noise amount selection value + dCounter = 0; // clear the counter + map[y][x] = cColor; // set pixel to color created above + } + else { + map[y][x] = 0; // otherwise, clear it } } } diff --git a/resources/rgbscripts/onebyone.js b/resources/rgbscripts/onebyone.js index ffce75c9e1..b76570b102 100644 --- a/resources/rgbscripts/onebyone.js +++ b/resources/rgbscripts/onebyone.js @@ -27,16 +27,24 @@ var testAlgo; algo.apiVersion = 2; algo.name = "One By One"; algo.author = "Jano Svitok"; - algo.acceptColors = 2; algo.properties = new Array(); algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); + for (var y = 0; y < height; y++) + { + map[y] = new Array(); + for (var x = 0; x < width; x++) + { + map[y][x] = 0; + } + } + var xx = step % width; var yy = (step - xx) / width; - HSVUtil.setPixel(map, width, xx, yy, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[yy][xx] = rgb; return map; }; diff --git a/resources/rgbscripts/opposite.js b/resources/rgbscripts/opposite.js index 9232df4720..4ad1d1d934 100644 --- a/resources/rgbscripts/opposite.js +++ b/resources/rgbscripts/opposite.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Opposite"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); algo.properties.push("name:orientation|type:list|display:Orientation|values:Horizontal,Vertical|write:setOrientation|read:getOrientation"); @@ -46,10 +45,10 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if (algo.orientation === 1) @@ -57,13 +56,17 @@ var testAlgo; if ((x % 2) === 0) { if (y === step) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } else { if (y === ((height - 1) - step)) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -72,13 +75,17 @@ var testAlgo; if ((y % 2) === 0) { if (x === step) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } else { if (x === ((width - 1) - step)) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/plasma.js b/resources/rgbscripts/plasma.js index 147e95e0b4..52b7008197 100644 --- a/resources/rgbscripts/plasma.js +++ b/resources/rgbscripts/plasma.js @@ -53,6 +53,7 @@ var testAlgo; var util = new Object; util.initialized = false; util.gradientData = new Array(); + util.colorArray = new Array(); algo.setPreset = function(_preset) { @@ -94,7 +95,7 @@ var testAlgo; algo.setSize = function(_size) { - algo.presetSize = parseFloat(_size); + algo.presetSize = _size; util.initialized = false; }; @@ -105,7 +106,7 @@ var testAlgo; algo.setRamp = function(_ramp) { - algo.ramp = parseFloat(_ramp); + algo.ramp = _ramp; util.initialized = false; }; @@ -116,7 +117,7 @@ var testAlgo; algo.setStep = function(_step) { - algo.stepsize = parseFloat(_step); + algo.stepsize = _step; util.initialized = false; }; @@ -127,7 +128,8 @@ var testAlgo; util.initialize = function() { - // calculate the HSV gradient for the selected preset + // calculate the gradient for the selected preset + // with the given width var gradIdx = 0; util.gradientData = new Array(); var colorArray = algo.rgbMapGetColors(); @@ -136,27 +138,29 @@ var testAlgo; var sColor = colorArray[i]; var eColor = colorArray[(i + 1) % colorArray.length]; if (! sColor) - sColor = {h: 0, s: 0, v: 0}; + sColor = 0; if (! eColor) - eColor = {h: 0, s: 0, v: 0}; + eColor = 0; - util.gradientData[gradIdx++] = {h: sColor.h, s: sColor.s, v: sColor.v}; + util.gradientData[gradIdx++] = sColor; + var sr = (sColor >> 16) & 0x00FF; + var sg = (sColor >> 8) & 0x00FF; + var sb = sColor & 0x00FF; + var er = (eColor >> 16) & 0x00FF; + var eg = (eColor >> 8) & 0x00FF; + var eb = eColor & 0x00FF; - // Shortest arc for hue interpolation - var dh = eColor.h - sColor.h; - if (dh > 0.5) dh -= 1; - if (dh < -0.5) dh += 1; - var stepH = dh / 300; - var stepS = (eColor.s - sColor.s) / 300; - var stepV = (eColor.v - sColor.v) / 300; + var stepR = ((er - sr) / 300); + var stepG = ((eg - sg) / 300); + var stepB = ((eb - sb) / 300); for (var s = 1; s < 300; s++) { - var gh = (sColor.h + stepH * s) % 1; - if (gh < 0) gh += 1; - var gs = sColor.s + stepS * s; - var gv = sColor.v + stepV * s; - util.gradientData[gradIdx++] = {h: gh, s: gs, v: gv}; + var gradR = Math.floor(sr + (stepR * s)) & 0x00FF; + var gradG = Math.floor(sg + (stepG * s)) & 0x00FF; + var gradB = Math.floor(sb + (stepB * s)) & 0x00FF; + var gradRGB = (gradR << 16) + (gradG << 8) + gradB; + util.gradientData[gradIdx++] = gradRGB; } } util.permArray = new Array(512); @@ -258,61 +262,50 @@ var testAlgo; return scaled; } - algo.rgbMapSetColors = function(rawColors) {}; + algo.rgbMapSetColors = function(rawColors) + { + if (! Array.isArray(rawColors)) + return; + + util.colorArray = new Array(rawColors.length); + for (var i = 0; i < util.colorArray.length; i++) { + if (i < rawColors.length) + { + util.colorArray[i] = rawColors[i]; + } else { + util.colorArray[i] = 0; + } + } + util.initialized = false; + } algo.rgbMapGetColors = function() { if (algo.presetIndex === 1) { - // Fire - return [ - {h: 0.1667, s: 1, v: 1}, - {h: 0, s: 1, v: 1}, - {h: 0.6667, s: 1, v: 0.251}, - {h: 0, s: 1, v: 1} - ]; + return [ 0xFFFF00, 0xFF0000, 0x000040, 0xFF0000 ]; } else if (algo.presetIndex === 2) { - // Abstract - return [ - {h: 0.639, s: 0.667, v: 1}, - {h: 0.5, s: 1, v: 1}, - {h: 0.8333, s: 1, v: 1}, - {h: 0.1667, s: 1, v: 1} - ]; + return [ 0x5571FF, 0x00FFFF, 0xFF00FF, 0xFFFF00 ]; } else if (algo.presetIndex === 3) { - // Ocean - return [ - {h: 0.614, s: 1, v: 0.725}, - {h: 0.514, s: 0.992, v: 1} - ]; + return [ 0x003AB9, 0x02EAFF ]; } else if (algo.presetIndex === 4) { - // User Defined - if (algo.colors && algo.colors.length > 0) + if (util.colorArray.length <= 0) { - return algo.colors; + // No user defined colors set, return default + return [ 0x00FF00, 0xFFAA00, 0x0000FF, 0xFFFF00, 0xFFFFFF ]; } - return [ - {h: 0.3333, s: 1, v: 1}, - {h: 0.111, s: 1, v: 1}, - {h: 0.6667, s: 1, v: 1}, - {h: 0.1667, s: 1, v: 1}, - {h: 0, s: 0, v: 1} - ]; + return util.colorArray; } else { - // Rainbow (presetIndex === 0 or default) - return [ - {h: 0, s: 1, v: 1}, - {h: 0.3333, s: 1, v: 1}, - {h: 0.6667, s: 1, v: 1} - ]; + // Either presetIndex === 0 or an invalid presetIndex + return [ 0xFF0000, 0x00FF00, 0x0000FF ]; } } @@ -333,9 +326,10 @@ var testAlgo; // keep the patten square var square = (width > height) ? width : height; - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { @@ -343,13 +337,7 @@ var testAlgo; var ny = y / square; var n = util.noise(size * nx, size * ny, algo.bstepcount); var gradStep = Math.round(Math.pow(n, (algo.ramp / 10)) * util.gradientData.length); - var color = util.gradientData[gradStep]; - if (color) { - var i = (y * width + x) * 3; - map[i] = color.h; - map[i + 1] = color.s; - map[i + 2] = color.v; - } + map[y][x] = util.gradientData[gradStep]; } } diff --git a/resources/rgbscripts/randomcolumn.js b/resources/rgbscripts/randomcolumn.js index 73e530ffdf..8dae53a303 100644 --- a/resources/rgbscripts/randomcolumn.js +++ b/resources/rgbscripts/randomcolumn.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Column"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -47,22 +46,28 @@ var testAlgo; var map = new Array(length); for (var i = 0; i < length; i++) { - map[i] = (si === i) ? 1 : 0; + if (si === i) { + map[i] = 1; + } else { + map[i] = 0; + } } return map; }; - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { if (step[x] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -91,7 +96,7 @@ var testAlgo; algo.height = height; } - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/randomfillcolumn.js b/resources/rgbscripts/randomfillcolumn.js index 6d4be9b9b5..d16e69f3c6 100644 --- a/resources/rgbscripts/randomfillcolumn.js +++ b/resources/rgbscripts/randomfillcolumn.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Fill Column"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -59,16 +58,18 @@ var testAlgo; return map; }; - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { if (step[x] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -99,7 +100,7 @@ var testAlgo; algo.height = height; } - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/randomfillrow.js b/resources/rgbscripts/randomfillrow.js index faf0937559..a0cde6dd24 100644 --- a/resources/rgbscripts/randomfillrow.js +++ b/resources/rgbscripts/randomfillrow.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Fill Row"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -59,16 +58,18 @@ var testAlgo; return map; }; - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { if (step[y] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -99,7 +100,7 @@ var testAlgo; algo.height = height; } - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/randomfillsingle.js b/resources/rgbscripts/randomfillsingle.js index 7548873153..9048583a77 100644 --- a/resources/rgbscripts/randomfillsingle.js +++ b/resources/rgbscripts/randomfillsingle.js @@ -32,7 +32,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Fill Single"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -63,17 +62,18 @@ var testAlgo; */ util.createStep = function(width, height, sy, sx, lastStep) { - var map = new Array(width * height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { if (sy === y && sx === x) { - map[y * width + x] = 1; - } else if (lastStep !== 0 && lastStep[y * width + x] === 1) { - map[y * width + x] = 1; + map[y][x] = 1; + } else if (lastStep !== 0 && lastStep[y][x] === 1) { + map[y][x] = 1; } else { - map[y * width + x] = 0; + map[y][x] = 0; } } } @@ -82,18 +82,20 @@ var testAlgo; }; /** - * Create map with HSV color where mask is active + * Create map with the real rgb value (because it now changes at each step) */ - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { - if (step[y * width + x] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + if (step[y][x] !== 0) { + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -144,7 +146,7 @@ var testAlgo; // refresh with real color - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; /** diff --git a/resources/rgbscripts/randompixelperrow.js b/resources/rgbscripts/randompixelperrow.js index e88704349e..e1f5116353 100644 --- a/resources/rgbscripts/randompixelperrow.js +++ b/resources/rgbscripts/randompixelperrow.js @@ -38,11 +38,17 @@ var testAlgo; * @return A two-dimensional array[height][width]. */ algo.rgbMap = function (width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = []; var index = Math.floor(Math.random() * (width)); - HSVUtil.setPixel(map, width, index, y, h, s, v); + for (var x = 0; x < width; x++) { + if (x === index) { + map[y][x] = rgb; + } else { + map[y][x] = 0; + } + } } return map; diff --git a/resources/rgbscripts/randompixelperrowmulticolor.js b/resources/rgbscripts/randompixelperrowmulticolor.js index 8c51faa804..1a95ac3af7 100644 --- a/resources/rgbscripts/randompixelperrowmulticolor.js +++ b/resources/rgbscripts/randompixelperrowmulticolor.js @@ -38,14 +38,24 @@ var testAlgo; * @return A two-dimensional array[height][width]. */ algo.rgbMap = function (width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = []; + //get a random pixel in the curent row (y) var index = Math.floor(Math.random() * (width)); - // Random HSV color - var h = Math.random(); - var s = 0.5 + Math.random() * 0.5; - var v = 0.5 + Math.random() * 0.5; - HSVUtil.setPixel(map, width, index, y, h, s, v); + + var gradR = Math.floor(Math.random() * (255)); + var gradG = Math.floor(Math.random() * (255)); + var gradB = Math.floor(Math.random() * (255)); + var gradRGB = (gradR << 16) + (gradG << 8) + gradB; + + for (var x = 0; x < width; x++) { + if (x === index) { + map[y][x] = gradRGB; //if this is the random pixel from above, set it to the user's chosen color + } else { + map[y][x] = 0; //otherwise, turn it off + } + } } return map; diff --git a/resources/rgbscripts/randomrow.js b/resources/rgbscripts/randomrow.js index 07e065050a..61e440ea7f 100644 --- a/resources/rgbscripts/randomrow.js +++ b/resources/rgbscripts/randomrow.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Row"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -47,22 +46,28 @@ var testAlgo; var map = new Array(length); for (var i = 0; i < length; i++) { - map[i] = (si === i) ? 1 : 0; + if (si === i) { + map[i] = 1; + } else { + map[i] = 0; + } } return map; }; - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { if (step[y] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -91,7 +96,7 @@ var testAlgo; algo.height = height; } - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/randomsingle.js b/resources/rgbscripts/randomsingle.js index b4975f583f..01d2023548 100644 --- a/resources/rgbscripts/randomsingle.js +++ b/resources/rgbscripts/randomsingle.js @@ -32,7 +32,6 @@ var testAlgo; algo.apiVersion = 1; algo.name = "Random Single"; algo.author = "David Garyga"; - algo.acceptColors = 2; algo.width = 0; algo.height = 0; @@ -63,12 +62,17 @@ var testAlgo; */ util.createStep = function(width, height, sy, sx) { - var map = new Array(width * height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { - map[y * width + x] = (sy === y && sx === x) ? 1 : 0; + if (sy === y && sx === x) { + map[y][x] = 1; + } else { + map[y][x] = 0; + } } } @@ -76,18 +80,20 @@ var testAlgo; }; /** - * Create map with HSV color where mask is active + * Create map with the real rgb value (because it now changes at each step) */ - util.createStepHsv = function(width, height, step) + util.createStepRgb = function(width, height, step, rgb) { - var map = HSVUtil.createMap(width, height); - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(width); for (var x = 0; x < width; x++) { - if (step[y * width + x] !== 0) { - HSVUtil.setPixel(map, width, x, y, h, s, v); + if (step[y][x] !== 0) { + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } @@ -136,7 +142,7 @@ var testAlgo; // refresh with real color - return util.createStepHsv(width, height, algo.steps[step]); + return util.createStepRgb(width, height, algo.steps[step], rgb); }; /** diff --git a/resources/rgbscripts/sinewave.js b/resources/rgbscripts/sinewave.js index 689b35ea73..09043b3fa9 100644 --- a/resources/rgbscripts/sinewave.js +++ b/resources/rgbscripts/sinewave.js @@ -71,7 +71,13 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); + for (var y = 0; y < height; y++) + { + map[y] = new Array(); + for (var x = 0; x < width; x++) + map[y][x] = 0; + } var stepsHeight = algo.orientation === 0 ? height : width; var stepsWidth = algo.orientation === 0 ? width : height; @@ -92,9 +98,9 @@ var testAlgo; sinPos--; if (algo.orientation === 0) - HSVUtil.setPixel(map, width, cStep, sinPos, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[sinPos][cStep] = rgb; else - HSVUtil.setPixel(map, width, sinPos, cStep, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[cStep][sinPos] = rgb; // fill the previous column/row // to create continuous painting @@ -108,9 +114,9 @@ var testAlgo; for (var f = startY; f < stopY; f++) { if (algo.orientation === 0) - HSVUtil.setPixel(map, width, col, f, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[f][col] = rgb; else - HSVUtil.setPixel(map, width, f, col, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[col][f] = rgb; } } } diff --git a/resources/rgbscripts/snowbubbles.js b/resources/rgbscripts/snowbubbles.js index 1999632be7..2823650564 100644 --- a/resources/rgbscripts/snowbubbles.js +++ b/resources/rgbscripts/snowbubbles.js @@ -29,7 +29,7 @@ var testAlgo; algo.author = "Hans-Jürgen Tappe"; algo.properties = []; algo.acceptColors = 1; - algo.presetColor = {h: 0, s: 0, v: 0}; + algo.presetColor = 0x000000; // number of flakes on screen at one time (default) algo.presetflakes = 20; algo.properties.push("name:presetflakes|type:range|display:Number of Flakes (10-255)|values:10,255|write:setAmount|read:getAmount"); @@ -77,8 +77,10 @@ var testAlgo; algo.setWindchill = function(_wind) { if (_wind === "Yes") { + // Random Colored flakes algo.windchill = 1; } else { + // flakes are chosen color algo.windchill = 0; } }; @@ -95,8 +97,10 @@ var testAlgo; algo.setReverse = function(_reverse) { if (_reverse === "Yes") { + // Random Colored flakes algo.reverse = 1; } else { + // flakes are chosen color algo.reverse = 0; } }; @@ -115,19 +119,27 @@ var testAlgo; // random position function for new flake function getNewNumberRange(minVal, maxVal) { + // Search in the range of min to max + 1 + // which will be reduced by random() excluding 1 + // and floor() reducing to lower number. return Math.floor(Math.random() * (maxVal + 1 - minVal)) + minVal; } - // set color of flake - if multicolor, choose random color + // set color of flake - if multicolor, choose random color. If not + // random, return user chosen color function getNewColor(isMultiColor, zColor) { if (isMultiColor === 1) { - return { - h: Math.random(), - s: 0.7 + Math.random() * 0.3, - v: 1 - }; + // random red level + var tr = Math.round(Math.random() * 255); + // random green level + var tg = Math.round(Math.random() * 255); + // random blue level + var tb = Math.round(Math.random() * 255); + // returned combined color + return (tr << 16) + (tg << 8) + tb; } else { + // If not multicolor, return chosen color return zColor; } } @@ -179,8 +191,15 @@ var testAlgo; util.initialize(width, height); } - var map = HSVUtil.createMap(width, height); - + // Clear map data for a blank map + var map = new Array(height); + for (var y = 0; y < height; y++) { + map[y] = new Array(); + for (var x = 0; x < width; x++) { + map[y][x] = 0; + } + } + algo.speedX = updateFlakeSpeedx(algo.speedX); // Start moving the flakes by looping through this routine, @@ -195,7 +214,7 @@ var testAlgo; flakes[i].y = height - 1; } flakes[i].z = getNewNumberRange(0, depth - 1); - flakes[i].c = getNewColor(algo.multiColor, algo.colors[0]); + flakes[i].c = getNewColor(algo.multiColor,rgb); flakes[i].s = getFlakeSpeedy(flakes[i].z); } @@ -209,24 +228,38 @@ var testAlgo; px = Math.floor(px); py = Math.floor(py); - var fc = flakes[i].c; + // parse out individual colors of current flake color + var r = (flakes[i].c >> 16) & 0x00FF; + var g = (flakes[i].c >> 8) & 0x00FF; + var b = flakes[i].c & 0x00FF; // if flake is far away, then it should be darker var colorLevel = 1 - (flakes[i].z / depth); + // Get the current channel values + var pRGB = map[py][px]; + var pr = (pRGB >> 16) & 0x00FF; + var pg = (pRGB >> 8) & 0x00FF; + var pb = pRGB & 0x00FF; + // Adjust flake brightness level based on how far away it is - var fv = fc.v * colorLevel; - - // Max-blend: the brighter pixel wins - var idx = (py * width + px) * 3; - var existV = map[idx + 2]; - if (fv > existV) { - map[idx] = fc.h; - map[idx + 1] = fc.s; - map[idx + 2] = fv; - } + // and by chosen flake color + r = Math.floor(r * colorLevel); + g = Math.floor(g * colorLevel); + b = Math.floor(b * colorLevel); + + // The higher value wins + r = Math.max(r, pr); + g = Math.max(g, pg); + b = Math.max(b, pb); + + // put all the individual rgb colors back together + pRGB = (r << 16) + (g << 8) + b; + + // store the flake's combined color in the map + map[py][px] = pRGB; } - + // new position of flake flakes[i].x += algo.speedX; flakes[i].y += flakes[i].s; diff --git a/resources/rgbscripts/squares.js b/resources/rgbscripts/squares.js index ab27dbc8ae..f9166bacbd 100644 --- a/resources/rgbscripts/squares.js +++ b/resources/rgbscripts/squares.js @@ -46,7 +46,7 @@ var testAlgo; this.xCenter = x; this.yCenter = y; this.step = step; - this.color = {h: 0, s: 0, v: 0}; + this.color = 0; } algo.setAmount = function(_amount) @@ -100,39 +100,67 @@ var testAlgo; util.initialized = true; }; - util.getStepColor = function(step, color) + util.getStepColor = function(step, rgb) { if (algo.fadeMode === 0) { - return {h: color.h, s: color.s, v: color.v}; + return rgb; } else { + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + var stepCount = Math.floor(util.squaresMaxSize / 2); var fadeStep = step; if (algo.fadeMode === 2) { fadeStep = stepCount - step; } - var factor = fadeStep / stepCount; - return {h: color.h, s: color.s, v: color.v * factor}; + var newR = Math.round((r / stepCount) * fadeStep); + var newG = Math.round((g / stepCount) * fadeStep); + var newB = Math.round((b / stepCount) * fadeStep); + var newRGB = (newR << 16) + (newG << 8) + newB; + return newRGB; } }; - // Merge HSV: keep brighter pixel (higher V) - util.mergeHsv = function(map, idx, h, s, v) + util.mergeRgb = function(rgb1, rgb2) { - var existV = map[idx + 2]; - if (existV <= 0) { - map[idx] = h; map[idx + 1] = s; map[idx + 2] = v; - } else if (v > existV) { - map[idx] = h; map[idx + 1] = s; map[idx + 2] = v; + if (rgb1 === 0) { + return rgb2; + } else if (rgb2 === 0) { + return rgb1; } - }; + // split rgb into components + var r1 = (rgb1 >> 16) & 0x00FF; + var g1 = (rgb1 >> 8) & 0x00FF; + var b1 = rgb1 & 0x00FF; + + var r2 = (rgb2 >> 16) & 0x00FF; + var g2 = (rgb2 >> 8) & 0x00FF; + var b2 = rgb2 & 0x00FF; + + var r = Math.max(r1, r2); + var g = Math.max(g1, g2); + var b = Math.max(b1, b2); + + return ((r << 16) + (g << 8) + b); + } - util.getNextStep = function(width, height) + util.getNextStep = function(width, height, rgb) { - // create an empty map (Float32Array, 3 floats per pixel) - var map = HSVUtil.createMap(width, height); + // create an empty, black map + var map = new Array(height); + + for (var y = 0; y < height; y++) + { + map[y] = new Array(); + for (var x = 0; x < width; x++) + { + map[y][x] = 0; + } + } for (var i = 0; i < algo.squaresAmount; i++) { @@ -143,9 +171,9 @@ var testAlgo; if (seed > 50) { continue; } squares[i].xCenter = Math.floor(Math.random() * width); squares[i].yCenter = Math.floor(Math.random() * height); - squares[i].color = {h: algo.colors[0].h, s: algo.colors[0].s, v: algo.colors[0].v}; - var idx = (squares[i].yCenter * width + squares[i].xCenter) * 3; - util.mergeHsv(map, idx, squares[i].color.h, squares[i].color.s, squares[i].color.v); + squares[i].color = rgb; + map[squares[i].yCenter][squares[i].xCenter] = + util.mergeRgb(map[squares[i].yCenter][squares[i].xCenter], squares[i].color); } else { @@ -163,14 +191,12 @@ var testAlgo; if (sx < 0 || sx >= width) { continue; } if (sy === firstY || sy === firstY + side || algo.fillSquares === 1) { - var idx = (sy * width + sx) * 3; - util.mergeHsv(map, idx, color.h, color.s, color.v); + map[sy][sx] = util.mergeRgb(map[sy][sx], color); } else { if (sx === firstX || sx === firstX + side) { - var idx = (sy * width + sx) * 3; - util.mergeHsv(map, idx, color.h, color.s, color.v); + map[sy][sx] = util.mergeRgb(map[sy][sx], color); } } } @@ -200,7 +226,7 @@ var testAlgo; } } - return util.getNextStep(width, height); + return util.getNextStep(width, height, rgb); }; algo.rgbMapStepCount = function(width, height) diff --git a/resources/rgbscripts/squaresfromcenter.js b/resources/rgbscripts/squaresfromcenter.js index e8554a69c3..a83789a281 100644 --- a/resources/rgbscripts/squaresfromcenter.js +++ b/resources/rgbscripts/squaresfromcenter.js @@ -51,26 +51,31 @@ var testAlgo; var isWidthEven = (width % 2 === 0); var isHeightEven = (height % 2 === 0); - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if (algo.fillSquares === 1) { if ((x <= widthCenter + step + (isWidthEven ? 1 : 0 ) && x >= widthCenter - step) && (y <= heightCenter + step + (isHeightEven ? 1 : 0) && y >= heightCenter - step)) { - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } else { if ((x === widthCenter + step + (isWidthEven ? 1 : 0 ) || x === widthCenter - step) && (y <= heightCenter + step + (isHeightEven ? 1 : 0) && y >= heightCenter - step)) { - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[y][x] = rgb; } else if ((y === heightCenter + step + (isHeightEven ? 1 : 0 ) || y === heightCenter - step) && (x <= widthCenter + step + (isWidthEven ? 1 : 0) && x >= widthCenter - step)) { - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/starfield.js b/resources/rgbscripts/starfield.js index d2bc8a53cc..8dbba244cd 100644 --- a/resources/rgbscripts/starfield.js +++ b/resources/rgbscripts/starfield.js @@ -29,7 +29,7 @@ var testAlgo; algo.author = "Doug Puckett+Branson Matheson"; algo.properties = []; algo.acceptColors = 1; - algo.presetColor = {h: 0, s: 0, v: 0}; + algo.presetColor = 0x000000; algo.properties.push("name:StarsAmount|type:range|display:Number of Stars (10-255)|values:10,255|write:setAmount|read:getAmount"); algo.presetStars = 50; // 50 stars on screen at one time (default) algo.properties.push("name:MultiColor|type:list|display:MultiColored Stars?|values:No,Yes|write:setMulti|read:getMulti"); @@ -90,15 +90,15 @@ var testAlgo; return Math.floor(Math.random() * (maxVal - minVal + 1)) + minVal; } - //set color of star - if multicolor, choose random color + //set color of star - if multicolor, choose random color. If not random, return user chosen color function getNewColor(isMultiColor, zColor) { if (isMultiColor === 1) { - return { - h: Math.random(), - s: 0.7 + Math.random() * 0.3, - v: 1 - }; + var tr = Math.floor(Math.random() * 255); // random red level + var tg = Math.floor(Math.random() * 255); // random green level + var tb = Math.floor(Math.random() * 255); // random blue level + return (tr << 16) + (tg << 8) + tb; // returned combined color } else { + // If not multicolor, return chosen color return zColor; } } @@ -124,50 +124,79 @@ var testAlgo; util.initialize(width, height); } - var map = HSVUtil.createMap(width, height); + // Clear map data for a blank map + var map = new Array(height); + + for (var y = 0; y < height; y++) { + map[y] = new Array(); + for (var x = 0; x < width; x++) { + map[y][x] = 0; + } + } // find center of display var halfWidth = width / 2; var halfHeight = height / 2; - // start moving the stars + // start moving the stars by looping through this routine, addressing each star individually (i is the star number) for (var i = 0; i < algo.presetStars - 1; i++) { // decrease depth on each pass through if (height >= width) { stars[i].z -= height / (height / 4); } else { stars[i].z -= width / (width / 4); } - // if star is off screen, create a new star near center + // if star is off screen (depth is zero or less) then create a new star near the center of the display if (stars[i].z <= 0) { stars[i].x = getNewNumberRange(-10, 10); stars[i].y = getNewNumberRange(-10, 10); stars[i].z = depth; - stars[i].c = getNewColor(algo.multiColor, algo.colors[0]); + stars[i].c = getNewColor(algo.multiColor, rgb); } // calculate the stars next position - var k = 200 / stars[i].z; - var px = Math.floor(stars[i].x * k + halfWidth); - var py = Math.floor(stars[i].y * k + halfHeight); + var k = 200 / stars[i].z; // how far away the star is + var px = Math.floor(stars[i].x * k + halfWidth); // x position of star + var py = Math.floor(stars[i].y * k + halfHeight); // y position of star if (px >= 0 && px < width && py >= 0 && py < height) { - // Brightness factor based on depth - var factor; + // if star is in the center, then it should be darker (farther away) + // and lighten as it moves "closer" (out to the edges) if (algo.invertColor == 1) { - factor = Math.max(0, Math.min(1, stars[i].z * 2 / 255)); + var colorLevel = (stars[i].z * 2); } else { - factor = Math.max(0, Math.min(1, 1 - stars[i].z * 2 / 255)); + var colorLevel = (255 - (stars[i].z * 2)); } - px = Math.floor(px); + // parse out individual colors of current star color + var r = (stars[i].c >> 16) & 0x00FF; + var g = (stars[i].c >> 8) & 0x00FF; + var b = stars[i].c & 0x00FF; + + // Adjust star brightness level based on how far away it is and by chosen star color + var rr = r - (255 - colorLevel); + var gg = g - (255 - colorLevel); + var bb = b - (255 - colorLevel); + + // Limit each color element to the maximum for chosen color or make 0 if below 0 + if (rr > r) { rr = r; } + if (rr < 0) { rr = 0; } + if (gg > g) { gg = g; } + if (gg < 0) { gg = 0; } + if (bb > b) { bb = b; } + if (bb < 0) { bb = 0; } + + // put all the individual rgb colors back together + var pRGB = (rr << 16) + (gg << 8) + bb; + px = Math.floor(px); // get rid of x and y position fractions py = Math.floor(py); - HSVUtil.setPixel(map, width, px, py, - stars[i].c.h, stars[i].c.s, stars[i].c.v * factor); + map[py][px] = pRGB; // store the star's combined color in the map + // console.log("i: " + i + " px: " + px + " py: " + py + " pz: " + stars[i].z + " color: " + r+g+b); } else { - stars[i].z = 0; // force a new star + // console.log("i: " + i + " k: " + k + " px: " + px + " width: " + width + " py: " + py + " height: " + height); + stars[i].z = 0; // if here, the star was outside screen for some reason so force a new star } } - return map; + return map; // return the map back to QLC+ }; diff --git a/resources/rgbscripts/stripes.js b/resources/rgbscripts/stripes.js index 372895cab5..d27eaaeb8c 100644 --- a/resources/rgbscripts/stripes.js +++ b/resources/rgbscripts/stripes.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Stripes"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -47,14 +46,17 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { if ((algo.orientation === 0 && x === step) || (algo.orientation === 1 && y === step)) { - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/stripesfromcenter.js b/resources/rgbscripts/stripesfromcenter.js index 7c71aea66c..776a62f737 100644 --- a/resources/rgbscripts/stripesfromcenter.js +++ b/resources/rgbscripts/stripesfromcenter.js @@ -27,7 +27,6 @@ var testAlgo; algo.apiVersion = 2; algo.name = "Stripes From Center"; algo.author = "Massimo Callegari"; - algo.acceptColors = 2; algo.orientation = 0; algo.properties = new Array(); @@ -55,15 +54,18 @@ var testAlgo; isEven = (width % 2 === 0); } - var map = HSVUtil.createMap(width, height); + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { var cmpAxis = (algo.orientation ? y : x); if (cmpAxis === center + step + (isEven ? 1 : 0 ) || cmpAxis === center - step) { - HSVUtil.setPixel(map, width, x, y, algo.colors[0].h, algo.colors[0].s, algo.colors[0].v); + map[y][x] = rgb; + } else { + map[y][x] = 0; } } } diff --git a/resources/rgbscripts/strobe.js b/resources/rgbscripts/strobe.js index fae8f81663..3683a149e3 100644 --- a/resources/rgbscripts/strobe.js +++ b/resources/rgbscripts/strobe.js @@ -47,17 +47,15 @@ var testAlgo; */ algo.rgbMap = function (width, height, rgb, step) { - var map = HSVUtil.createMap(width, height); - if ((step % algo.frequency) === 0) + var map = new Array(); + for (var y = 0; y < height; y++) { - var h = algo.colors[0].h, s = algo.colors[0].s, v = algo.colors[0].v; - for (var y = 0; y < height; y++) + var _map = new Array(); + for (var x = 0; x < width; x++) { - for (var x = 0; x < width; x++) - { - HSVUtil.setPixel(map, width, x, y, h, s, v); - } + _map.push((step % algo.frequency) !== 0 ? 0 : rgb); } + map.push(_map); } return map; }; diff --git a/resources/rgbscripts/verticalfall.js b/resources/rgbscripts/verticalfall.js index 5da3f7698c..e4f4cfc5c0 100644 --- a/resources/rgbscripts/verticalfall.js +++ b/resources/rgbscripts/verticalfall.js @@ -33,15 +33,21 @@ var testAlgo; util.initialized = false; util.width = 0; util.height = 0; - util.color = {h: 0, s: 0, v: 0}; + util.color = 0xFF0000; var fallObject = new Array(); var objYPos = new Array(); - var objmap; + var objmap = new Array(); - util.initialize = function(width, height) + util.initialize = function(rgb, width, height) { - var ch = algo.colors[0].h, cs = algo.colors[0].s, cv = algo.colors[0].v; + var r = (rgb >> 16) & 0x00FF; + var g = (rgb >> 8) & 0x00FF; + var b = rgb & 0x00FF; + + var rStep = (r / height); + var gStep = (g / height); + var bStep = (b / height); objYPos = new Array(width); for (var i = 0; i < width; i++) { @@ -49,17 +55,26 @@ var testAlgo; } fallObject = new Array(height); - fallObject[0] = {h: ch, s: cs, v: cv}; - fallObject[height - 1] = {h: 0, s: 0, v: 0}; + fallObject[0] = rgb; + fallObject[height - 1] = 0; for (var f = 1; f < height - 1; f++) { - var factor = (height - f - 1) / height; - fallObject[f] = {h: ch, s: cs, v: cv * factor}; + var stepRGB = (rStep * (height - f - 1)) << 16; + stepRGB += (gStep * (height - f - 1)) << 8; + stepRGB += (bStep * (height - f - 1)); + fallObject[f] = stepRGB; } - objmap = HSVUtil.createMap(width, height); + objmap = new Array(height); + for (var y = 0; y < height; y++) + { + objmap[y] = new Array(width); + for (var x = 0; x < width; x++) { + objmap[y][x] = 0; + } + } - util.color = {h: ch, s: cs, v: cv}; + util.color = rgb; util.width = width; util.height = height; util.initialized = true; @@ -86,11 +101,7 @@ var testAlgo; { if (yPos < height) { - var fo = fallObject[i]; - var i3 = (yPos * util.width + x) * 3; - objmap[i3] = fo.h; - objmap[i3 + 1] = fo.s; - objmap[i3 + 2] = fo.v; + objmap[yPos][x] = fallObject[i]; } yPos--; if (yPos === -1) { break; } @@ -107,10 +118,9 @@ var testAlgo; algo.rgbMap = function(width, height, rgb, step) { - if (util.initialized === false || - util.color.h !== algo.colors[0].h || util.color.s !== algo.colors[0].s || util.color.v !== algo.colors[0].v || + if (util.initialized === false || util.color !== rgb || util.width != width || util.height != height) { - util.initialize(width, height); + util.initialize(rgb, width, height); } return util.getNextStep(width, height); diff --git a/resources/rgbscripts/waves.js b/resources/rgbscripts/waves.js index 0dfbcb3f0d..b5307ba97f 100644 --- a/resources/rgbscripts/waves.js +++ b/resources/rgbscripts/waves.js @@ -29,7 +29,6 @@ function() algo.apiVersion = 2; algo.name = "Waves"; algo.author = "Nathan Durnan"; - algo.acceptColors = 2; algo.properties = new Array(); algo.taillength = 50; @@ -116,13 +115,10 @@ function() var tailSteps = Math.round(span * algo.taillength/100); if (tailSteps === 0) { tailSteps = 1; } - var map = HSVUtil.createMap(width, height); - var baseH = algo.colors[0].h; - var baseS = algo.colors[0].s; - var baseV = algo.colors[0].v; - + var map = new Array(height); for (var y = 0; y < height; y++) { + map[y] = new Array(); for (var x = 0; x < width; x++) { var pos = ((algo.orientation === 0) ? x : y); @@ -173,16 +169,18 @@ function() break; } // Determine the fade for this pixel - if (fill) + var thisRgb = rgb; + if (fill && (algo.tailfade === 1)) { - var v = baseV; - if (algo.tailfade === 1) - { - var thisTailStep = Math.round(util.fadeSteps * (step - stepPos) / tailSteps); - v = baseV * util.fadeObject[thisTailStep]; - } - HSVUtil.setPixel(map, width, x, y, baseH, baseS, v); + var thisTailStep = Math.round(util.fadeSteps * (step - stepPos) / tailSteps); + var r = Math.round(((rgb >> 16) & 0x00FF) * util.fadeObject[thisTailStep]); + var g = Math.round(((rgb >> 8) & 0x00FF) * util.fadeObject[thisTailStep]); + var b = Math.round((rgb & 0x00FF) * util.fadeObject[thisTailStep]); + thisRgb = (r << 16) + (g << 8) + b; } + // Populate the matrix + map[y][x] = (fill ? thisRgb : 0); + map[y][x] = (fill ? thisRgb : 0); } } return map; diff --git a/ui/src/rgbmatrixeditor.cpp b/ui/src/rgbmatrixeditor.cpp index 31207d2a9a..0f3b8ce8a8 100644 --- a/ui/src/rgbmatrixeditor.cpp +++ b/ui/src/rgbmatrixeditor.cpp @@ -453,9 +453,7 @@ void RGBMatrixEditor::updateColors() m_mtxColor4Button->setIcon(QIcon(pm)); m_mtxColor5Button->setIcon(QIcon(pm)); } - else if (m_controlModeCombo->currentIndex() != RGBMatrix::ControlModeRgb && - m_controlModeCombo->currentIndex() != RGBMatrix::ControlModeRgbw && - m_controlModeCombo->currentIndex() != RGBMatrix::ControlModeRgbwBrighter) + else if (m_controlModeCombo->currentIndex() != RGBMatrix::ControlModeRgb) { // Convert color 1 to grayscale for single color modes uchar gray = qGray(m_matrix->getColor(0).rgb()); @@ -1291,25 +1289,6 @@ void RGBMatrixEditor::slotSaveToSequenceClicked() grpScene->setValue(head.fxi, rgb.at(2), 0); } } - else if (m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeRgbw || - m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeRgbwBrighter) - { - QLCFixtureHead fHead = fxi->head(head.head); - quint32 rCh = fHead.channelNumber(QLCChannel::Red, QLCChannel::MSB); - quint32 gCh = fHead.channelNumber(QLCChannel::Green, QLCChannel::MSB); - quint32 bCh = fHead.channelNumber(QLCChannel::Blue, QLCChannel::MSB); - quint32 wCh = fHead.channelNumber(QLCChannel::White, QLCChannel::MSB); - - if (rCh != QLCChannel::invalid() && gCh != QLCChannel::invalid() && bCh != QLCChannel::invalid()) - { - grpScene->setValue(head.fxi, rCh, 0); - grpScene->setValue(head.fxi, gCh, 0); - grpScene->setValue(head.fxi, bCh, 0); - - if (wCh != QLCChannel::invalid()) - grpScene->setValue(head.fxi, wCh, 0); - } - } else if (m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeDimmer) { quint32 channel = fxi->masterIntensityChannel(); @@ -1420,31 +1399,6 @@ void RGBMatrixEditor::slotSaveToSequenceClicked() step.values.append(SceneValue(head.fxi, cmy.at(2), cmyCol.yellow())); } } - else if (m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeRgbw || - m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeRgbwBrighter) - { - bool subtractWhite = (m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeRgbw); - QLCFixtureHead fHead = fxi->head(head.head); - quint32 rCh = fHead.channelNumber(QLCChannel::Red, QLCChannel::MSB); - quint32 gCh = fHead.channelNumber(QLCChannel::Green, QLCChannel::MSB); - quint32 bCh = fHead.channelNumber(QLCChannel::Blue, QLCChannel::MSB); - quint32 wCh = fHead.channelNumber(QLCChannel::White, QLCChannel::MSB); - - uchar r = uchar(qRed(col)); - uchar g = uchar(qGreen(col)); - uchar b = uchar(qBlue(col)); - uchar w = wCh == QLCChannel::invalid() ? 0 : qMin(r, qMin(g, b)); - - if (rCh != QLCChannel::invalid() && gCh != QLCChannel::invalid() && bCh != QLCChannel::invalid()) - { - step.values.append(SceneValue(head.fxi, rCh, subtractWhite ? uchar(r - w) : r)); - step.values.append(SceneValue(head.fxi, gCh, subtractWhite ? uchar(g - w) : g)); - step.values.append(SceneValue(head.fxi, bCh, subtractWhite ? uchar(b - w) : b)); - - if (wCh != QLCChannel::invalid()) - step.values.append(SceneValue(head.fxi, wCh, w)); - } - } else if (m_controlModeCombo->currentIndex() == RGBMatrix::ControlModeDimmer) { quint32 channel = fxi->masterIntensityChannel(); diff --git a/ui/src/rgbmatrixeditor.ui b/ui/src/rgbmatrixeditor.ui index ee9ae4734d..d966295e3d 100644 --- a/ui/src/rgbmatrixeditor.ui +++ b/ui/src/rgbmatrixeditor.ui @@ -483,23 +483,13 @@ Dimmer - - - Shutter - - - - - RGBW (Accurate) - - - - - RGBW (Brighter) - - - - + + + Shutter + + + + From 79274bf77eb539ac4c98e9c21209e9e8f8fc415f Mon Sep 17 00:00:00 2001 From: Andre Bossard Date: Mon, 10 Aug 2026 16:49:27 +0200 Subject: [PATCH 2/2] Add HUEMatrixEditor QML component and associated resources - Introduced a new QML component `HUEMatrixEditor.qml` for editing hue matrix functions, featuring various UI elements for configuration. - Created a new CMakeLists.txt file for managing hue script resources, ensuring scripts are copied to the appropriate directory when building for Android. - Added a new SVG icon `huematrix.svg` to visually represent the hue matrix in the application. Signed-off-by: Andre Bossard --- .../extensions/qlcplus-dev/package-lock.json | 6 - MANUAL_REVIEW.md | 57 + README.md | 29 +- engine/src/CMakeLists.txt | 5 + engine/src/doc.cpp | 10 + engine/src/doc.h | 4 + engine/src/function.cpp | 17 + engine/src/function.h | 3 +- engine/src/huecolor.h | 107 + engine/src/huematrix.cpp | 1724 +++++++++++++++++ engine/src/huematrix.h | 422 ++++ engine/src/huescript.cpp | 600 ++++++ engine/src/huescript.h | 137 ++ engine/src/huescriptscache.cpp | 121 ++ engine/src/huescriptscache.h | 85 + engine/src/jsthread_p.h | 53 + engine/src/qlcconfig.h.in | 2 + engine/src/qlcconfig.h.noroot.in | 2 + engine/src/rgbalgorithm.h | 17 + engine/src/rgbmatrix.h | 27 +- engine/src/rgbscriptscache.cpp | 3 + engine/src/rgbscriptv4.h | 7 +- engine/src/showfunction.cpp | 1 + engine/test/CMakeLists.txt | 1 + engine/test/function/function_test.cpp | 3 + engine/test/huematrix/CMakeLists.txt | 38 + engine/test/huematrix/huematrix_test.cpp | 1316 +++++++++++++ engine/test/huematrix/huematrix_test.h | 110 ++ engine/test/huematrix/test.sh | 4 + mcp/test/rgb_transform_test.cpp | 54 +- mcp/tools/conversions.h | 49 +- mcp/tools/function_tools.cpp | 121 +- mcp/tools/query_tools.cpp | 8 +- qmlui/CMakeLists.txt | 1 + qmlui/app.cpp | 9 + qmlui/functionmanager.cpp | 23 +- qmlui/functionmanager.h | 5 +- qmlui/huematrixeditor.cpp | 1084 +++++++++++ qmlui/huematrixeditor.h | 254 +++ qmlui/importmanager.cpp | 9 + .../qml/fixturesfunctions/AddFunctionMenu.qml | 6 + .../qml/fixturesfunctions/FunctionManager.qml | 12 + .../qml/fixturesfunctions/HUEMatrixEditor.qml | 1221 ++++++++++++ qmlui/qmlui.qrc | 1 + qmlui/showmanager.cpp | 2 + resources/CMakeLists.txt | 1 + resources/huescripts/CMakeLists.txt | 57 + resources/huescripts/audiospectrum.js | 2 +- resources/icons/svg/huematrix.svg | 27 + resources/icons/svg/svgicons.qrc | 1 + ui/src/app.cpp | 9 + unittest.sh | 1 + variables.cmake | 20 + 53 files changed, 7767 insertions(+), 121 deletions(-) create mode 100644 engine/src/huecolor.h create mode 100644 engine/src/huematrix.cpp create mode 100644 engine/src/huematrix.h create mode 100644 engine/src/huescript.cpp create mode 100644 engine/src/huescript.h create mode 100644 engine/src/huescriptscache.cpp create mode 100644 engine/src/huescriptscache.h create mode 100644 engine/src/jsthread_p.h create mode 100644 engine/test/huematrix/CMakeLists.txt create mode 100644 engine/test/huematrix/huematrix_test.cpp create mode 100644 engine/test/huematrix/huematrix_test.h create mode 100755 engine/test/huematrix/test.sh create mode 100644 qmlui/huematrixeditor.cpp create mode 100644 qmlui/huematrixeditor.h create mode 100644 qmlui/qml/fixturesfunctions/HUEMatrixEditor.qml create mode 100644 resources/huescripts/CMakeLists.txt create mode 100644 resources/icons/svg/huematrix.svg diff --git a/.github/extensions/qlcplus-dev/package-lock.json b/.github/extensions/qlcplus-dev/package-lock.json index 296dc623d6..078c9343f3 100644 --- a/.github/extensions/qlcplus-dev/package-lock.json +++ b/.github/extensions/qlcplus-dev/package-lock.json @@ -145,12 +145,6 @@ "node": ">= 10" } }, - "node_modules/@webviewjs/webview/node_modules/@webviewjs/webview-freebsd-x64": { - "optional": true - }, - "node_modules/@webviewjs/webview/node_modules/@webviewjs/webview-linux-x64-gnu": { - "optional": true - }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", diff --git a/MANUAL_REVIEW.md b/MANUAL_REVIEW.md index c86d8d0a2d..fa5d0c5747 100644 --- a/MANUAL_REVIEW.md +++ b/MANUAL_REVIEW.md @@ -570,6 +570,12 @@ Navigate to Virtual Console with VC editing access: - **Song Manager — artist/BPM/key metadata:** Artist, BPM, and key fields in the song list are placeholders. The Show name embeds "Artist - Title" but structured extraction is not yet implemented. - **Song Manager — folder path:** Songs are identified by Shows in the `Songs/` function folder. Manually placing non-song Shows in that folder will cause them to appear in the Song Manager. - **Page input modes — multiple Normal pages:** If multiple Normal pages define mappings for the same MIDI channel/key, all matching Normal pages will fire when Inherit-mode fallback triggers. This is by design (they all contribute to the "global pool"), but can cause double-triggers if the same widget binding exists on two Normal pages. +- **HUEMatrix — existing workspaces lose audio algorithms:** The 41 HSV audio scripts moved from `resources/rgbscripts/` to `resources/huescripts/`, so pre-existing `RGBMatrix` functions that referenced them no longer resolve (56 functions across `GARAGE.qxw`, `LOADDDD.qxw`, `G2.qxw`). A warning naming both the script and the function is emitted on load. By design — no automatic migration; recreate them as `HUEMatrix` functions. +- **HUEMatrix — fork-only `` is dropped from RGBMatrix:** `RGBMatrix` is now byte-identical to upstream, which does not know that tag, so it is discarded on load with an `Unknown RGB matrix tag` warning (5 functions in `GARAGE.qxw`). Reassign the audio profile on the recreated `HUEMatrix`. +- **HUEMatrix — "Audio Spectrum" script renamed:** The HSV script that shadowed the built-in `RGBAudio` algorithm is now "Audio Spectrum Bars", so the built-in is reachable again by name. Workspaces storing the old script name will not resolve it. +- **HUEMatrix — VC Animation widget icon:** A `HUEMatrix` can be assigned to a Virtual Console Animation widget (`HUEMatrix` IS-A `RGBMatrix`), but the widget icon is unconditionally the RGB Matrix icon. Cosmetic only. +- **HUEMatrix — editor algorithm list is not covered by tests:** `qmlui` builds an executable rather than a library and `FunctionEditor` depends on `Tardis`, so the editor cannot be constructed in a unit test. `HUEMatrixEditor::algorithms()` delegating to `HUEMatrix::availableAlgorithms()` is verified only at the cache boundary — §23.1 covers it manually. +- **HUEMatrix — shutdown drain:** Destroying a `HUEMatrix` waits up to 2 s per object for an in-flight async precompute task that never ran, then warns and continues. Many stuck matrices would add up at shutdown; not observed in practice. --- @@ -1059,3 +1065,54 @@ the intended hardware. | Area | Tester | Date | Pass / Fail | Notes | |------|--------|------|-------------|-------| | Live audio BPM, silence, beat timing, Aubio continuity, external lock | | | | §22.1–22.3 | +--- + +## 23. HUEMatrix fork and RGBMatrix upstream restore + +> **Automated coverage:** `huematrix_test` (42 cases) owns the HSV `Float32Array` +> contract, the dual packed-uint contract, fork-property in-memory and XML +> round-trips, algorithm-list separation, icon-site enumeration, built-in +> reachability, bounded destructor drain, async-precompute generation checks, +> per-tick recompute for audio algorithms, and the unavailable-algorithm and +> `AudioProfileID` load warnings. `rgbmatrix_test` (9) and `rgbscript_test` (14) +> are upstream's own suites, unmodified, and prove the restore. +> `mcp_rgb_transform_test` (15) covers rotation/mirror/beat spatially. +> Do not repeat those mechanical assertions here. +> +> `engine/src/rgbmatrix.cpp` is byte-identical to `upstream/master` +> (`git diff upstream/master -- engine/src/rgbmatrix.cpp` is empty). The items +> below are the visual, hardware, and workspace-migration judgments automation +> cannot make. + +### 23.1 Both matrix types are selectable and distinct [LOW RISK] + +- ☐ Open the Add Function menu → both "RGB Matrix" and "HUE Matrix" are offered, with visibly different icons +- ☐ Filter the function tree by each type in turn → the two filter buttons show different icons and each lists only its own functions +- ☐ Create a HUE Matrix and open its editor → the algorithm dropdown lists the 41 audio effects **and** the upstream stock patterns +- ☐ Create an RGB Matrix and open its editor → the algorithm dropdown lists **only** upstream stock patterns, with no `Audio *` entries + +### 23.2 RGBMatrix behaves as pristine upstream [MEDIUM RISK] + +- ☐ Run several stock patterns (Stripes, Plasma, Gradient, Fireworks) on an RGB Matrix and compare against expected upstream behaviour → colour, motion, and step timing show no fork-specific artifacts +- ☐ Confirm the fork-only controls (rotation, mirror, beat effect, brightness, RGBW control modes) are **absent** from the RGB Matrix editor → they belong to HUE Matrix only + +### 23.3 HUEMatrix audio effects on hardware [DMX] [MEDIUM RISK] + +- ☐ Play familiar material through a HUE Matrix running Aurora, Equalizer, Fire, and Water → hue, saturation, and value read correctly on the physical fixture, with no banding or clipping introduced by the HSV→RGB conversion +- ☐ Exercise rotation, mirror + mirror blend, and a beat effect while audio is running → transforms compose with the audio response without tearing, stutter, or dropped frames +- ☐ Select the built-in "Audio Spectrum" (not the "Audio Spectrum Bars" script) on a HUE Matrix → it renders as the built-in algorithm + +### 23.4 Existing workspace migration [HIGH RISK] + +- ☐ Load a workspace that predates the fork (e.g. `GARAGE.qxw`) with the console visible → warnings name each function that lost its algorithm, and each dropped `AudioProfileID`; the app does not crash +- ☐ Recreate one affected effect as a HUE Matrix and confirm it renders as before → the migration path is workable by hand +- ☐ Save and reload the migrated workspace → the HUE Matrix keeps its algorithm, transforms, beat settings, and control mode + +### 23.5 Sign-off + +| Area | Tester | Date | Pass / Fail | Notes | +|------|--------|------|-------------|-------| +| Type selection and editor lists | | | | §23.1 | +| RGBMatrix upstream parity | | | | §23.2 | +| HUEMatrix audio on hardware | | | | §23.3 | +| Legacy workspace migration | | | | §23.4 | diff --git a/README.md b/README.md index 32b1e69f81..af4ff9bdb1 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ > - **Enhanced Function Wizard** — rainbow/warm/cool palettes, beat-synced chasers, movement patterns, prism/focus/zoom detection, per-fixture VC pages, QLCPalette generation > - Launchpad controller integration support > - Audio capture / BPM detection for scripts -> - **22 audio-reactive RGB scripts** (LedFX-ported atmospheric effects, strobes, motion, EQ visualizers) -> - **RGB Matrix rotation & mirroring** (engine-level, all algorithm types) -> - **RGB Matrix low-latency step transitions** (~3ms vs ~22ms previously) +> - **41 audio-reactive HSV scripts** (LedFX-ported atmospheric effects, strobes, motion, EQ visualizers) — in `resources/huescripts/`, offered to **HUE Matrix** only +> - **HUE Matrix — a new function type forked from RGB Matrix.** `RGBMatrix` is restored byte-identical to upstream QLC+; all fork behaviour (HSV script contract, rotation/mirror, beat transforms, brightness, RGBW modes, audio scripts) lives in `HUEMatrix`, which inherits from it. Both are selectable side by side. +> - **HUE Matrix rotation & mirroring** (engine-level, all algorithm types) +> - **HUE Matrix low-latency step transitions** (~3ms vs ~22ms previously) > - **Blend mode ordering fix** for Mask/Subtractive blend modes > - **Enhanced OS2L plugin** — Bonjour/mDNS auto-discovery, song metadata, connection status LED, web diagnostics dashboard > - **Auto-reload last workspace** on startup (no `--openlast` flag needed) @@ -42,7 +43,7 @@ > - **FineFractions for all editors** — RGB Matrix, EFX, Scene, and Speed Dial preset editors now show 1/4, 1/8, 1/16 beat subdivisions (previously limited to 1/1 and 1/2) > - **Update Scene from Live** — DMX Dump dialog: "Update only scene channels from live" button captures current pre-GM DMX values into an existing scene, scoped to only the channels already in the scene (preserves layer separation, with Tardis undo) > - **Speed Dial multiply mode** — factor buttons (1/16x–16x) multiply existing function speeds instead of replacing them; preserves authored fadeIn/hold/fadeOut ratios; one-click reset to originals; works with both Time and Beats mode functions -> - **RGB Matrix RGBW mode** — new `RGBW (Accurate)` and `RGBW (Brighter)` control modes drive R, G, B, AND White channels simultaneously. Accurate extracts white (`W=min(R,G,B)`, subtract from RGB); Brighter keeps RGB full and adds white on top. Works with any RGBW fixture — not fixture-specific. +> - **HUE Matrix RGBW mode** — `RGBW (Accurate)` and `RGBW (Brighter)` control modes drive R, G, B, AND White channels simultaneously. Accurate extracts white (`W=min(R,G,B)`, subtract from RGB); Brighter keeps RGB full and adds white on top. Works with any RGBW fixture — not fixture-specific. > - **Keyboard shortcuts** — 20+ shortcuts ported from v4: Ctrl+N/O/S (New/Open/Save), Ctrl+Z/Shift+Z (Undo/Redo), Ctrl+Shift+Esc (Panic/Stop All), F11 (Fullscreen), Alt+1–6 (view switching), Ctrl+PgUp/PgDown (cycle views), Ctrl+[/] (drawer toggle), Function Manager (Delete/Clone/Wizard), Show Manager (Space/Ctrl+Space play/stop, copy/paste). Platform-aware tooltips (⌘ on macOS). Guards for text editing, popups, kiosk mode. > - **DDP multi-universe sync fix** — eliminated frame-queue desync that caused 4+ DDP universes to display out of order. Replaced unreliable cross-thread batching with immediate per-universe send (sub-millisecond gap, PUSH per universe — matches Art-Net behavior). > @@ -64,8 +65,9 @@ > 1/1 and 1/2 beat subdivisions (`ByTwoFractions`). All four now use `FineFractions`, > exposing the full 1/1, 1/2, 1/4, 1/8, 1/16 range — matching Chaser editor behavior. > -> #### RGB Matrix RGBW Control Modes -> Two new control modes for RGBW fixtures: +> #### HUE Matrix RGBW Control Modes +> Two control modes for RGBW fixtures, available on **HUE Matrix** (`RGBMatrix` is +> byte-identical to upstream and does not offer them): > > | Mode | Algorithm | Best for | > |------|-----------|----------| @@ -101,9 +103,10 @@ > Each universe's data is sent as soon as it arrives (PUSH flag per universe). > Inter-universe gap is sub-millisecond. Matches Art-Net behavior (no sync packet). > -> #### RGB Matrix Rotation & Mirroring -> Rotation and mirroring are now engine-level properties on `RGBMatrix`, available -> for **all** algorithm types (Plain, Script, Text, Image, Audio). +> #### HUE Matrix Rotation & Mirroring +> Rotation and mirroring are engine-level properties on `HUEMatrix`, available +> for **all** algorithm types (Plain, Script, Text, Image, Audio). They are not +> present on `RGBMatrix`, which is byte-identical to upstream. > > | Property | Values | Description | > |----------|--------|-------------| @@ -794,6 +797,10 @@ cd build # Engine tests (beat quantization) cmake --build . --target beatquantize_test -j8 && ./engine/test/beatquantize/beatquantize_test +# HUE Matrix / RGB Matrix (run from each suite's own build dir - resource paths are cwd-relative) +cmake --build . --target huematrix_test -j8 && (cd engine/test/huematrix && ./huematrix_test) +cmake --build . --target rgbmatrix_test -j8 && (cd engine/test/rgbmatrix && ./rgbmatrix_test) + # MCP tests cmake --build . --target mcp_conversions_test -j8 && ./mcp/test/mcp_conversions_test cmake --build . --target mcp_vc_query_filter_test -j8 && ./mcp/test/mcp_vc_query_filter_test @@ -806,6 +813,10 @@ cmake --build . --target mcp_vc_validation_test -j8 && ./mcp/test/mcp_vc_validat | `mcp_conversions_test` | 43 | Beat string parsing/formatting, decimal precision, off-grid snapping, GCD reduction, round-trips | | `mcp_vc_query_filter_test` | — | VC widget query filtering and pagination | | `mcp_vc_validation_test` | — | Widget type/field validation | +| `huematrix_test` | 42 | HUE Matrix: HSV `Float32Array` contract, dual packed-uint contract, algorithm-list separation, fork properties + XML round-trip, icon-site enumeration, bounded destructor drain, async precompute, per-tick audio recompute, load warnings | +| `rgbmatrix_test` | 9 | Upstream's own suite, unmodified — proves the `RGBMatrix` restore | +| `rgbscript_test` | 14 | Upstream's own suite, unmodified — packed-uint script contract | +| `mcp_rgb_transform_test` | 15 | Rotation, mirror + blend, and beat transforms (spatial) | ### E2E tests (Web DMX Control Panel) diff --git a/engine/src/CMakeLists.txt b/engine/src/CMakeLists.txt index 356196ebe1..d768131210 100644 --- a/engine/src/CMakeLists.txt +++ b/engine/src/CMakeLists.txt @@ -58,6 +58,11 @@ add_library(${module_name} SHARED qlcphysical.cpp qlcphysical.h qlcpoint.cpp qlcpoint.h rgbalgorithm.cpp rgbalgorithm.h + rgbaudio.cpp rgbaudio.h + huematrix.cpp huematrix.h + huescript.cpp huescript.h + huescriptscache.cpp huescriptscache.h + huecolor.h jsthread_p.h rgbimage.cpp rgbimage.h rgbmatrix.cpp rgbmatrix.h rgbplain.cpp rgbplain.h diff --git a/engine/src/doc.cpp b/engine/src/doc.cpp index 61f408249a..b71eb9a48c 100644 --- a/engine/src/doc.cpp +++ b/engine/src/doc.cpp @@ -40,6 +40,7 @@ #include "oscaudiosource.h" #include "audiochannel.h" #include "rgbscriptscache.h" +#include "huescriptscache.h" #include "channelsgroup.h" #include "scriptwrapper.h" #include "collection.h" @@ -68,6 +69,7 @@ Doc::Doc(QObject* parent, int universes) , m_fixtureDefCache(new QLCFixtureDefCache) , m_modifiersCache(new QLCModifiersCache) , m_rgbScriptsCache(new RGBScriptsCache(this)) + , m_hueScriptsCache(new HUEScriptsCache(this)) , m_ioPluginCache(new IOPluginCache(this)) , m_audioPluginCache(new AudioPluginCache(this)) , m_masterTimer(new MasterTimer(this)) @@ -128,6 +130,9 @@ Doc::~Doc() delete m_rgbScriptsCache; m_rgbScriptsCache = NULL; + + delete m_hueScriptsCache; + m_hueScriptsCache = NULL; } void Doc::clearContents() @@ -276,6 +281,11 @@ RGBScriptsCache* Doc::rgbScriptsCache() const return m_rgbScriptsCache; } +HUEScriptsCache* Doc::hueScriptsCache() const +{ + return m_hueScriptsCache; +} + IOPluginCache* Doc::ioPluginCache() const { return m_ioPluginCache; diff --git a/engine/src/doc.h b/engine/src/doc.h index 8c76b79b5b..06e7d1cf23 100644 --- a/engine/src/doc.h +++ b/engine/src/doc.h @@ -44,6 +44,7 @@ class AudioCapture; class AudioAnalyzer; class OscAudioSource; class RGBScriptsCache; +class HUEScriptsCache; class AudioPluginCache; class MonitorProperties; @@ -138,6 +139,8 @@ class Doc final : public QObject /** Get the RGB scripts cache object */ RGBScriptsCache *rgbScriptsCache() const; + HUEScriptsCache *hueScriptsCache() const; + /** Get the I/O plugin cache object */ IOPluginCache *ioPluginCache() const; @@ -169,6 +172,7 @@ class Doc final : public QObject QLCFixtureDefCache *m_fixtureDefCache; QLCModifiersCache *m_modifiersCache; RGBScriptsCache *m_rgbScriptsCache; + HUEScriptsCache *m_hueScriptsCache; IOPluginCache *m_ioPluginCache; AudioPluginCache *m_audioPluginCache; MasterTimer *m_masterTimer; diff --git a/engine/src/function.cpp b/engine/src/function.cpp index 11e9acfd71..373e5e712a 100644 --- a/engine/src/function.cpp +++ b/engine/src/function.cpp @@ -32,6 +32,7 @@ #include "mastertimer.h" #include "collection.h" #include "rgbmatrix.h" +#include "huematrix.h" #include "function.h" #include "sequence.h" #include "chaser.h" @@ -50,6 +51,7 @@ const QString KEFXString ( "EFX" ); const QString KCollectionString ( "Collection" ); const QString KScriptString ( "Script" ); const QString KRGBMatrixString ( "RGBMatrix" ); +const QString KHUEMatrixString ( "HUEMatrix" ); const QString KShowString ( "Show" ); const QString KSequenceString ( "Sequence" ); const QString KAudioString ( "Audio" ); @@ -248,6 +250,7 @@ QString Function::typeToString(Type type) case CollectionType: return KCollectionString; case ScriptType: return KScriptString; case RGBMatrixType: return KRGBMatrixString; + case HUEMatrixType: return KHUEMatrixString; case ShowType: return KShowString; case SequenceType: return KSequenceString; case AudioType: return KAudioString; @@ -272,6 +275,8 @@ Function::Type Function::stringToType(const QString& string) return ScriptType; else if (string == KRGBMatrixString) return RGBMatrixType; + else if (string == KHUEMatrixString) + return HUEMatrixType; else if (string == KShowString) return ShowType; else if (string == KSequenceString) @@ -1117,6 +1122,8 @@ bool Function::loader(QXmlStreamReader &root, Doc* doc) function = new class Script(doc); else if (type == Function::RGBMatrixType) function = new class RGBMatrix(doc); + else if (type == Function::HUEMatrixType) + function = new class HUEMatrix(doc); else if (type == Function::ShowType) function = new class Show(doc); else if (type == Function::SequenceType) @@ -1136,6 +1143,16 @@ bool Function::loader(QXmlStreamReader &root, Doc* doc) function->setLastEdited(lastEdited); if (function->loadXML(root) == true) { + // A matrix whose stored algorithm is unavailable to its type loads + // silently to nothing; say which function lost its algorithm. The + // missing algorithm's name is reported by the scripts cache. + RGBMatrix *matrix = qobject_cast (function); + if (matrix != NULL && matrix->algorithm() == NULL) + qWarning() << "Function" << name << "of type" + << Function::typeToString(type) + << "lost its algorithm: the stored algorithm is not" + << "available to this function type."; + if (doc->addFunction(function, id) == true) { /* Success */ diff --git a/engine/src/function.h b/engine/src/function.h index c41de10b26..240e0bf8ab 100644 --- a/engine/src/function.h +++ b/engine/src/function.h @@ -124,7 +124,8 @@ class Function : public QObject ShowType = 1 << 6, SequenceType = 1 << 7, AudioType = 1 << 8, - VideoType = 1 << 9 + VideoType = 1 << 9, + HUEMatrixType = 1 << 10 }; Q_ENUM(Type) diff --git a/engine/src/huecolor.h b/engine/src/huecolor.h new file mode 100644 index 0000000000..25e6419d00 --- /dev/null +++ b/engine/src/huecolor.h @@ -0,0 +1,107 @@ +/* + Q Light Controller Plus + huecolor.h + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUECOLOR_H +#define HUECOLOR_H + +#include +#include +#include + +#include +#include + +/** @addtogroup engine Engine + * @{ + */ + +/** + * HSV <-> packed RGB conversions shared by HUEScript and HUEMatrix. + * + * All components are floats in [0,1]; hue wraps. + */ +namespace HUEColor +{ + struct Hsv { float h, s, v; }; + + /** Convert HSV (each in [0,1]) to packed 0xAARRGGBB via qRgb(). */ + inline uint hsvToRgb(float h, float s, float v) + { + if (!std::isfinite(h)) h = 0.0f; + if (!std::isfinite(s)) s = 0.0f; + if (!std::isfinite(v)) v = 0.0f; + h = h - floorf(h); + if (h < 0) + h += 1.0f; + s = qBound(0.0f, s, 1.0f); + v = qBound(0.0f, v, 1.0f); + + float c = v * s; + float x = c * (1.0f - fabsf(fmodf(h * 6.0f, 2.0f) - 1.0f)); + float m = v - c; + float r, g, b; + int sector = (int)(h * 6.0f) % 6; + switch (sector) + { + case 0: r = c; g = x; b = 0; break; + case 1: r = x; g = c; b = 0; break; + case 2: r = 0; g = c; b = x; break; + case 3: r = 0; g = x; b = c; break; + case 4: r = x; g = 0; b = c; break; + default: r = c; g = 0; b = x; break; + } + return qRgb(qRound((r + m) * 255.0f), + qRound((g + m) * 255.0f), + qRound((b + m) * 255.0f)); + } + + /** Convert packed 0xRRGGBB to HSV floats in [0,1]. */ + inline Hsv rgbToHsv(uint packed) + { + float r = float((packed >> 16) & 0xFF) / 255.0f; + float g = float((packed >> 8) & 0xFF) / 255.0f; + float b = float(packed & 0xFF) / 255.0f; + float mx = std::max({r, g, b}); + float mn = std::min({r, g, b}); + float d = mx - mn; + float h = 0.0f; + float s = (mx == 0.0f) ? 0.0f : d / mx; + if (d != 0.0f) + { + if (mx == r) h = fmodf((g - b) / d + 6.0f, 6.0f) / 6.0f; + else if (mx == g) h = ((b - r) / d + 2.0f) / 6.0f; + else h = ((r - g) / d + 4.0f) / 6.0f; + } + return {h, s, mx}; + } + + /** Marshal an Hsv into a QJSValue {h,s,v} object. */ + inline QJSValue hsvToJs(QJSEngine *engine, const Hsv &hsv) + { + QJSValue obj = engine->newObject(); + obj.setProperty(QStringLiteral("h"), QJSValue(double(hsv.h))); + obj.setProperty(QStringLiteral("s"), QJSValue(double(hsv.s))); + obj.setProperty(QStringLiteral("v"), QJSValue(double(hsv.v))); + return obj; + } +} + +/** @} */ + +#endif // HUECOLOR_H diff --git a/engine/src/huematrix.cpp b/engine/src/huematrix.cpp new file mode 100644 index 0000000000..c30fae8dc4 --- /dev/null +++ b/engine/src/huematrix.cpp @@ -0,0 +1,1724 @@ +/* + Q Light Controller Plus + huematrix.cpp + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "huematrix.h" +#include "huescript.h" +#include "huescriptscache.h" +#include "rgbscriptscache.h" +#include "qlcfixturehead.h" +#include "audiochannel.h" +#include "audioprofile.h" +#include "fixturegroup.h" +#include "genericfader.h" +#include "fadechannel.h" +#include "rgbimage.h" +#include "rgbplain.h" +#include "rgbaudio.h" +#include "rgbtext.h" +#include "doc.h" + +#define KXMLQLCRGBMatrixStartColor QStringLiteral("MonoColor") +#define KXMLQLCRGBMatrixEndColor QStringLiteral("EndColor") +#define KXMLQLCRGBMatrixColor QStringLiteral("Color") +#define KXMLQLCRGBMatrixColorIndex QStringLiteral("Index") + +#define KXMLQLCRGBMatrixFixtureGroup QStringLiteral("FixtureGroup") +#define KXMLQLCRGBMatrixDimmerControl QStringLiteral("DimmerControl") + +#define KXMLQLCRGBMatrixProperty QStringLiteral("Property") +#define KXMLQLCRGBMatrixPropertyName QStringLiteral("Name") +#define KXMLQLCRGBMatrixPropertyValue QStringLiteral("Value") + +#define KXMLQLCRGBMatrixControlMode QStringLiteral("ControlMode") +#define KXMLQLCRGBMatrixControlModeRgb QStringLiteral("RGB") +#define KXMLQLCRGBMatrixControlModeAmber QStringLiteral("Amber") +#define KXMLQLCRGBMatrixControlModeWhite QStringLiteral("White") +#define KXMLQLCRGBMatrixControlModeUV QStringLiteral("UV") +#define KXMLQLCRGBMatrixControlModeDimmer QStringLiteral("Dimmer") +#define KXMLQLCRGBMatrixControlModeShutter QStringLiteral("Shutter") +#define KXMLQLCRGBMatrixControlModeRgbw QStringLiteral("RGBW") +#define KXMLQLCRGBMatrixControlModeRgbwBrighter QStringLiteral("RGBWBrighter") + +#define KXMLQLCRGBMatrixRotation QStringLiteral("Rotation") +#define KXMLQLCRGBMatrixMirror QStringLiteral("Mirror") +#define KXMLQLCRGBMatrixMirrorBlend QStringLiteral("MirrorBlend") +#define KXMLQLCRGBMatrixBrightness QStringLiteral("Brightness") + +#define KXMLQLCRGBMatrixBeatEffect QStringLiteral("BeatEffect") +#define KXMLQLCRGBMatrixBeatSelection QStringLiteral("BeatSelection") +#define KXMLQLCRGBMatrixBeatOrientation QStringLiteral("BeatOrientation") + +/**************************************************************************** + * Initialization + ****************************************************************************/ + +HUEMatrix::HUEMatrix(Doc *doc) + : RGBMatrix(doc) + , m_rotation(0) + , m_mirror(0) + , m_mirrorBlend(MirrorFlip) + , m_brightness(1.0) + , m_beatEffect(BeatEffectOff) + , m_beatSelection(BeatSelAllOnDownbeat) + , m_beatOrientation(BeatOrientRows) + , m_currentBeat(0) + , m_lastBeat(-1) + , m_randomSegment(0) +{ + m_type = Function::HUEMatrixType; + + // Precomputed-map state starts empty. m_currentGeneration begins at 0; + // consumers only trust the precomputed map when ready==1, so the initial + // value of m_precomputedGeneration is harmless until the first kick. + m_precomputedReady.storeRelease(0); + m_precomputedInFlight.storeRelease(0); + m_currentGeneration.storeRelease(0); + m_precomputedGeneration = 0; + m_precomputedStep = -1; + m_precomputedColor = 0; + m_precomputedAlgorithm = nullptr; + + setName(tr("New HUE Matrix")); + + // Mark the PixelPlan dirty initially. It will be (re)built lazily on the + // first updateMapChannels() call. Connect to Doc signals so that fixture + // address/mode changes and group membership changes invalidate the cache. + m_pixelPlanDirty.storeRelease(1); + if (doc != NULL) + { + connect(doc, &Doc::fixtureChanged, + this, &HUEMatrix::invalidatePixelPlan, Qt::DirectConnection); + connect(doc, &Doc::fixtureRemoved, + this, &HUEMatrix::invalidatePixelPlan, Qt::DirectConnection); + connect(doc, &Doc::fixtureGroupChanged, + this, &HUEMatrix::invalidatePixelPlan, Qt::DirectConnection); + connect(doc, &Doc::fixtureGroupRemoved, + this, &HUEMatrix::invalidatePixelPlan, Qt::DirectConnection); + } +} + +HUEMatrix::~HUEMatrix() +{ + // Any async precompute task captures `this`, so wait for it to drain before + // we tear down. A task normally completes within one rgbMap duration + // (~10ms), but the flag is raised *before* the job is queued: if the JS + // thread is torn down in between, the queued lambda never runs and the flag + // is never cleared. Bound the wait so shutdown cannot hang. + QElapsedTimer timer; + timer.start(); + while (m_precomputedInFlight.loadAcquire() != 0) + { + if (timer.elapsed() >= precomputeDrainTimeoutMs) + { + qWarning() << Q_FUNC_INFO << "async precompute did not drain within" + << precomputeDrainTimeoutMs << "ms for" << name() + << "- abandoning the wait"; + break; + } + QThread::yieldCurrentThread(); + } +} + + +QStringList HUEMatrix::availableAlgorithms(Doc *doc) +{ + QStringList list = RGBAlgorithm::algorithms(doc); + foreach (QString name, doc->hueScriptsCache()->hsvNames()) + { + if (list.contains(name) == false) + list << name; + } + return list; +} + +/** Names of the built-in (non-script) algorithms. A HUE script that declares + * one of these would shadow the built-in and make it unreachable by name. */ +static QStringList builtInAlgorithmNames(Doc *doc) +{ + RGBPlain plain(doc); + RGBText text(doc); + RGBImage image(doc); + RGBAudio audio(doc); + return QStringList() << plain.name() << text.name() << image.name() << audio.name(); +} + +RGBAlgorithm *HUEMatrix::createAlgorithm(Doc *doc, const QString &name) +{ + // Built-ins win over scripts. Without this a HUE script declaring e.g. + // "Audio Spectrum" would permanently hide the RGBAudio built-in. + if (builtInAlgorithmNames(doc).contains(name)) + return RGBAlgorithm::algorithm(doc, name); + + // Scripts must be resolved through the HUE cache so that they get the + // HSV contract and the audio machinery. Built-ins fall through to the + // upstream factory unchanged. + HUEScript *script = doc->hueScriptsCache()->script(name); + if (script != NULL) + return script; + + // The upstream factory returns an empty RGBScript for an unknown name. + // Reject unknown names here so that callers can detect them. + if (availableAlgorithms(doc).contains(name) == false) + return NULL; + + return RGBAlgorithm::algorithm(doc, name); +} + +RGBAlgorithm *HUEMatrix::algorithmLoader(Doc *doc, QXmlStreamReader &root) +{ + if (root.attributes().value(KXMLQLCRGBAlgorithmType).toString() != KXMLQLCRGBScript) + return RGBAlgorithm::loader(doc, root); + + QString name = root.readElementText(); + HUEScript *script = doc->hueScriptsCache()->script(name); + if (script == NULL) + qWarning() << Q_FUNC_INFO << "Script" << name << "not available"; + return script; +} + +QIcon HUEMatrix::getIcon() const +{ + return QIcon(":/huematrix.svg"); +} + +void HUEMatrix::setTotalDuration(quint32 msec) +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + + if (m_algorithm == NULL) + return; + + FixtureGroup *grp = doc()->fixtureGroup(fixtureGroup()); + if (grp == NULL) + return; + + int steps = m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)); + setDuration(msec / steps); +} + +quint32 HUEMatrix::totalDuration() +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + + if (m_algorithm == NULL) + return 0; + + FixtureGroup *grp = doc()->fixtureGroup(fixtureGroup()); + if (grp == NULL) + return 0; + + //qDebug () << "Algorithm steps:" << m_algorithm->rgbMapStepCount(grp->size()); + return m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)) * duration(); +} + +void HUEMatrix::setDimmerControl(bool dimmerControl) +{ + m_dimmerControl = dimmerControl; + m_pixelPlanDirty.storeRelease(1); +} + +Function* HUEMatrix::createCopy(Doc* doc, bool addToDoc) +{ + Q_ASSERT(doc != NULL); + + Function *copy = new RGBMatrix(doc); + if (copy->copyFrom(this) == false) + { + delete copy; + copy = NULL; + } + if (addToDoc == true && doc->addFunction(copy) == false) + { + delete copy; + copy = NULL; + } + + return copy; +} + +bool HUEMatrix::copyFrom(const Function* function) +{ + const HUEMatrix *mtx = qobject_cast (function); + if (mtx == NULL) + return false; + + setDimmerControl(mtx->dimmerControl()); + setFixtureGroup(mtx->fixtureGroup()); + + m_rgbColors.clear(); + foreach (QColor col, mtx->getColors()) + m_rgbColors.append(col); + + if (mtx->algorithm() != NULL) + setAlgorithm(mtx->algorithm()->clone()); + else + setAlgorithm(NULL); + + setControlMode(mtx->controlMode()); + setRotation(mtx->rotation()); + setMirror(mtx->mirror()); + setMirrorBlend(mtx->mirrorBlend()); + setBrightness(mtx->brightness()); + setBeatEffect(mtx->beatEffect()); + setBeatSelection(mtx->beatSelection()); + setBeatOrientation(mtx->beatOrientation()); + + return Function::copyFrom(function); +} + +void HUEMatrix::setFixtureGroup(quint32 id) +{ + m_fixtureGroupID = id; + { + QMutexLocker algoLocker(&m_algorithmMutex); + m_group = doc()->fixtureGroup(m_fixtureGroupID); + } + m_stepsCount = algorithmStepsCount(); + m_pixelPlanDirty.storeRelease(1); + // Phase 4: any precomputed map is for the OLD group. Invalidate. + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); +} + +void HUEMatrix::setAlgorithm(RGBAlgorithm *algo) +{ + RGBAlgorithm *oldAlgo = nullptr; + { + QMutexLocker algorithmLocker(&m_algorithmMutex); + oldAlgo = m_algorithm; + m_algorithm = algo; + + // Phase 4: invalidate any pending/stored precomputed frame BEFORE + // releasing the algorithm pointer for deletion. Bumping the generation + // ensures any in-flight JSThread task that captured the old generation + // will discard its result rather than store it. + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + + m_requestEngineCreation = true; + + /** If there's been a change of Script algorithm "on the fly", + * then re-apply the properties currently set in this RGBMatrix */ + if (m_algorithm != NULL && m_algorithm->type() == RGBAlgorithm::Script) + { + RGBScript *script = static_cast (m_algorithm); + QMapIterator it(m_properties); + while (it.hasNext()) + { + it.next(); + if (script->setProperty(it.key(), it.value()) == false) + { + /** If the new algorithm doesn't expose a property, + * then remove it from the cached list, otherwise + * it would be carried around forever (and saved on XML) */ + m_properties.take(it.key()); + } + } + + // Colors are injected via injectColors() at render time; + // no need to seed m_rgbColors from the script here. + } + } + // Phase 4: deferred delete of the previous algorithm. For Script-typed + // algos, this serializes after any pending precompute tasks on the + // JSThread (FIFO), avoiding use-after-free in the async path. + deferDeleteAlgorithm(oldAlgo); + m_stepsCount = algorithmStepsCount(); + + if (m_applyingStyleAttributes == false) + Function::adjustAttribute(algorithmIndex(), PatternAttr); + + emit changed(id()); +} + +int HUEMatrix::algorithmStepsCount() +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + + if (m_algorithm == NULL) + return 0; + + FixtureGroup *grp = doc()->fixtureGroup(fixtureGroup()); + if (grp != NULL) + return m_algorithm->rgbMapStepCount(effectiveAlgorithmSize(grp)); + + return 0; +} + +void HUEMatrix::previewMap(int step, RGBMatrixStep *handler) +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + if (m_algorithm == NULL || handler == NULL) + return; + + if (m_group == NULL) + m_group = doc()->fixtureGroup(fixtureGroup()); + + if (m_group != NULL) + { + QSize algoSize = effectiveAlgorithmSize(m_group); + if (m_algorithm->usesAudio()) + m_algorithm->setDisplaySize(m_group->size()); + setMapColors(m_algorithm); + m_algorithm->rgbMap(algoSize, handler->stepColor().rgb(), step, handler->m_map); + if (m_rotation || m_mirror) + applyTransforms(handler->m_map, algoSize, m_group->size(), + m_rotation, m_mirror, m_mirrorBlend); + } +} + + +bool HUEMatrix::loadXML(QXmlStreamReader &root) +{ + if (root.name() != KXMLQLCFunction) + { + qWarning() << Q_FUNC_INFO << "Function node not found"; + return false; + } + + if (root.attributes().value(KXMLQLCFunctionType).toString() != typeToString(Function::HUEMatrixType)) + { + qWarning() << Q_FUNC_INFO << "Function is not a HUE matrix"; + return false; + } + + /* Load matrix contents */ + while (root.readNextStartElement()) + { + if (root.name() == KXMLQLCFunctionSpeed) + { + loadXMLSpeed(root); + } + else if (root.name() == KXMLQLCFunctionTempoType) + { + loadXMLTempoType(root); + } + else if (root.name() == KXMLQLCRGBAlgorithm) + { + setAlgorithm(HUEMatrix::algorithmLoader(doc(), root)); + } + else if (root.name() == KXMLQLCRGBMatrixFixtureGroup) + { + setFixtureGroup(root.readElementText().toUInt()); + } + else if (root.name() == KXMLQLCFunctionDirection) + { + loadXMLDirection(root); + } + else if (root.name() == KXMLQLCFunctionRunOrder) + { + loadXMLRunOrder(root); + } + // Legacy support + else if (root.name() == KXMLQLCRGBMatrixStartColor) + { + setColor(0, QColor::fromRgb(QRgb(root.readElementText().toUInt()))); + } + else if (root.name() == KXMLQLCRGBMatrixEndColor) + { + setColor(1, QColor::fromRgb(QRgb(root.readElementText().toUInt()))); + } + else if (root.name() == KXMLQLCRGBMatrixColor) + { + int colorIdx = root.attributes().value(KXMLQLCRGBMatrixColorIndex).toInt(); + setColor(colorIdx, QColor::fromRgb(QRgb(root.readElementText().toUInt()))); + } + else if (root.name() == KXMLQLCRGBMatrixControlMode) + { + setControlMode(stringToControlMode(root.readElementText())); + } + else if (root.name() == KXMLQLCRGBMatrixProperty) + { + QString name = root.attributes().value(KXMLQLCRGBMatrixPropertyName).toString(); + QString value = root.attributes().value(KXMLQLCRGBMatrixPropertyValue).toString(); + setProperty(name, value); + root.skipCurrentElement(); + } + else if (root.name() == KXMLQLCRGBMatrixDimmerControl) + { + setDimmerControl(root.readElementText().toInt()); + } + else if (root.name() == KXMLQLCRGBMatrixRotation) + { + setRotation(root.readElementText().toInt()); + } + else if (root.name() == KXMLQLCRGBMatrixMirror) + { + setMirror(root.readElementText().toInt()); + } + else if (root.name() == KXMLQLCRGBMatrixMirrorBlend) + { + setMirrorBlend(stringToMirrorBlend(root.readElementText())); + } + else if (root.name() == KXMLQLCRGBMatrixBrightness) + { + setBrightness(root.readElementText().toDouble()); + } + else if (root.name() == KXMLQLCRGBMatrixBeatEffect) + { + setBeatEffect(stringToBeatEffect(root.readElementText())); + } + else if (root.name() == KXMLQLCRGBMatrixBeatSelection) + { + setBeatSelection(stringToBeatSelection(root.readElementText())); + } + else if (root.name() == KXMLQLCRGBMatrixBeatOrientation) + { + setBeatOrientation(stringToBeatOrientation(root.readElementText())); + } + else + { + qWarning() << Q_FUNC_INFO << "Unknown RGB matrix tag:" << root.name(); + root.skipCurrentElement(); + } + } + + return true; +} + +bool HUEMatrix::saveXML(QXmlStreamWriter *doc) const +{ + Q_ASSERT(doc != NULL); + + /* Function tag */ + doc->writeStartElement(KXMLQLCFunction); + + /* Common attributes */ + saveXMLCommon(doc); + + /* Tempo type */ + saveXMLTempoType(doc); + + /* Speeds */ + saveXMLSpeed(doc); + + /* Direction */ + saveXMLDirection(doc); + + /* Run order */ + saveXMLRunOrder(doc); + + /* Algorithm */ + if (m_algorithm != NULL) + m_algorithm->saveXML(doc); + + /* LEGACY - Dimmer Control */ + if (dimmerControl()) + doc->writeTextElement(KXMLQLCRGBMatrixDimmerControl, QString::number(dimmerControl())); + + /* Colors */ + for (int i = 0; i < m_rgbColors.count(); i++) + { + if (m_rgbColors.at(i).isValid() == false) + continue; + + doc->writeStartElement(KXMLQLCRGBMatrixColor); + doc->writeAttribute(KXMLQLCRGBMatrixColorIndex, QString::number(i)); + doc->writeCharacters(QString::number(m_rgbColors.at(i).rgb())); + doc->writeEndElement(); + } + + /* Control Mode */ + doc->writeTextElement(KXMLQLCRGBMatrixControlMode, HUEMatrix::controlModeToString(m_controlMode)); + + /* Fixture Group */ + doc->writeTextElement(KXMLQLCRGBMatrixFixtureGroup, QString::number(fixtureGroup())); + + /* Properties */ + QMapIterator it(m_properties); + while (it.hasNext()) + { + it.next(); + doc->writeStartElement(KXMLQLCRGBMatrixProperty); + doc->writeAttribute(KXMLQLCRGBMatrixPropertyName, it.key()); + doc->writeAttribute(KXMLQLCRGBMatrixPropertyValue, it.value()); + doc->writeEndElement(); + } + + /* Rotation & Mirror */ + if (m_rotation != 0) + doc->writeTextElement(KXMLQLCRGBMatrixRotation, QString::number(m_rotation)); + if (m_mirror != 0) + doc->writeTextElement(KXMLQLCRGBMatrixMirror, QString::number(m_mirror)); + if (m_mirrorBlend != MirrorFlip) + doc->writeTextElement(KXMLQLCRGBMatrixMirrorBlend, mirrorBlendToString(m_mirrorBlend)); + if (m_brightness != 1.0) + doc->writeTextElement(KXMLQLCRGBMatrixBrightness, QString::number(m_brightness)); + + /* Beat Transform */ + if (m_beatEffect != BeatEffectOff) + { + doc->writeTextElement(KXMLQLCRGBMatrixBeatEffect, beatEffectToString(m_beatEffect)); + doc->writeTextElement(KXMLQLCRGBMatrixBeatSelection, beatSelectionToString(m_beatSelection)); + doc->writeTextElement(KXMLQLCRGBMatrixBeatOrientation, beatOrientationToString(m_beatOrientation)); + } + + /* End the tag */ + doc->writeEndElement(); + + return true; +} + +void HUEMatrix::preRun(MasterTimer *timer) +{ + { + QMutexLocker algorithmLocker(&m_algorithmMutex); + + m_group = doc()->fixtureGroup(m_fixtureGroupID); + if (m_group == NULL) + { + // No fixture group to control + stop(FunctionParent::master()); + return; + } + + if (m_algorithm != NULL) + { + checkEngineCreation(); + + // Copy direction from parent class direction + m_stepHandler->initializeDirection(direction(), m_rgbColors[0], m_rgbColors[1], m_stepsCount, m_runAlgorithm); + + // Update continuous phase when starting playback + if (m_stepsCount > 0) + m_continuousPhase = double(m_stepHandler->currentStepIndex()) / double(m_stepsCount); + + if (m_runAlgorithm->type() == RGBAlgorithm::Script) + { + RGBScript *script = static_cast (m_runAlgorithm); + QMapIterator it(m_properties); + while (it.hasNext()) + { + it.next(); + script->setProperty(it.key(), it.value()); + } + } + else if (m_runAlgorithm->type() == RGBAlgorithm::Image) + { + RGBImage *image = static_cast (m_runAlgorithm); + if (image->animatedSource()) + image->rewindAnimation(); + } + } + } + + m_roundTime.restart(); + + // The resolved fixture group may have changed since last preRun; force the + // PixelPlan to be rebuilt on the first updateMapChannels() call. + m_pixelPlanDirty.storeRelease(1); + + // Phase 4: drop any precomputed map carried over from a previous run. + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + + Function::preRun(timer); +} + +bool HUEMatrix::advanceStep(MasterTimer *timer, quint32 &prevElapsed) +{ + // Refresh beat duration before any beat checks + if (tempoType() == Beats) + m_stepBeatDuration = beatsToTime(duration(), timer->beatTimeDuration()); + + prevElapsed = elapsed(); + incrementElapsed(); + + if (tempoType() == Time) + return elapsed() >= duration() ? roundCheckLocked() : false; + + if (tempoType() != Beats) + return false; + + if (timer->isBeat()) + { + incrementElapsedBeats(); + if (elapsedBeats() % duration() != 0) + return false; + + bool stepChanged = roundCheckLocked(); + resetElapsed(); + return stepChanged; + } + + if (elapsed() >= m_stepBeatDuration && (uint)timer->timeToNextBeat() > m_stepBeatDuration / 16) + return roundCheckLocked(); + + return false; +} + +int HUEMatrix::updateBeatPhase(MasterTimer *timer) +{ + int beatsPerBar = 4; + + if (m_beatEffect == BeatEffectOff) + return beatsPerBar; + + AudioProfile *profile = doc()->audioProfile(doc()->activeAudioProfileId()); + if (profile == NULL) + profile = doc()->defaultAudioProfile(); + AudioChannel *channel = (profile != NULL) ? profile->channel() : NULL; + if (profile != NULL) + beatsPerBar = std::clamp(profile->channelConfig().aubio.beatsPerBar, 1, 8); + + // Prefer the analysed bar phase, fall back to counting timer beats + AudioSnapshot snap; + if (channel != NULL) + snap = channel->snapshot(); + + if (channel != NULL && snap.music.barPhase > 0) + m_currentBeat = int(snap.music.barPhase) % beatsPerBar; + else if (timer->isBeat()) + m_currentBeat = (m_currentBeat + 1) % beatsPerBar; + + // Latch random segment on beat change + if (m_beatSelection == BeatSelRandom && m_currentBeat != m_lastBeat) + { + m_randomSegment = QRandomGenerator::global()->bounded(beatsPerBar); + m_lastBeat = m_currentBeat; + } + + return beatsPerBar; +} + +bool HUEMatrix::consumePrecomputedMap(const QSize &algoSize, uint stepColor, + int stepIndex, quint32 generation) +{ + if (m_precomputedReady.loadAcquire() != 1) + return false; + + QMutexLocker pre(&m_precomputedMutex); + + bool matches = m_precomputedReady.loadAcquire() == 1 + && m_precomputedGeneration == generation + && m_precomputedAlgorithm == m_runAlgorithm + && m_precomputedStep == stepIndex + && m_precomputedColor == stepColor + && m_precomputedAlgoSize == algoSize; + + // Either consumed or stale: the slot is free again in both cases. + if (matches) + { + m_stepHandler->m_map = std::move(m_precomputedMap); + m_precomputedMap = RGBMap(); + } + m_precomputedReady.storeRelease(0); + + return matches; +} + +void HUEMatrix::write(MasterTimer *timer, QList universes) +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + + if (m_group == NULL) + { + // No fixture group to control + stop(FunctionParent::master()); + return; + } + + // No time to do anything. + if (duration() == 0) + return; + + if (m_algorithm != NULL && m_requestEngineCreation) + checkEngineCreation(); + + // Invalid/nonexistent script + if (m_runAlgorithm == NULL || m_runAlgorithm->apiVersion() == 0) + return; + + if (isPaused()) + return; + + quint32 prevElapsed = 0; + bool stepChanged = advanceStep(timer, prevElapsed); + int beatsPerBar = updateBeatPhase(timer); + + // Recompute when: step just changed, first tick of a step, audio-reactive, + // or beat transform active. Otherwise the previous map still holds. + bool needsRecompute = stepChanged + || prevElapsed < MasterTimer::tick() + || m_runAlgorithm->usesAudio() + || m_beatEffect != BeatEffectOff; + if (!needsRecompute) + return; + + QSize algoSize = effectiveAlgorithmSize(m_group); + uint stepColor = m_stepHandler->stepColor().rgb(); + int stepIndex = m_stepHandler->currentStepIndex(); + quint32 generation = m_currentGeneration.loadAcquire(); + + // A pre-computed frame already has rotation/mirror applied (those are + // stable across ticks). The beat transform is never pre-computed because + // m_currentBeat is only known on this thread, at this tick. + if (consumePrecomputedMap(algoSize, stepColor, stepIndex, generation) == false) + { + if (m_runAlgorithm->usesAudio()) + m_runAlgorithm->setDisplaySize(m_group->size()); + m_runAlgorithm->rgbMap(algoSize, stepColor, stepIndex, m_stepHandler->m_map); + if (m_rotation || m_mirror) + applyTransforms(m_stepHandler->m_map, algoSize, m_group->size(), + m_rotation, m_mirror, m_mirrorBlend); + } + + if (m_beatEffect != BeatEffectOff) + applyBeatTransform(m_stepHandler->m_map, m_currentBeat, beatsPerBar); + + updateMapChannels(m_stepHandler->m_map, m_group, universes, timer->beatTimeDuration()); + + // Kick off pre-computation for the NEXT tick, assuming it will use the + // same step and colour (true for most ticks). If that assumption is wrong, + // consumePrecomputedMap() detects the mismatch and falls back to the + // synchronous path above. + kickAsyncRgbMap(m_runAlgorithm, algoSize, stepColor, stepIndex, generation); +} + +void HUEMatrix::postRun(MasterTimer *timer, QList universes) +{ + uint fadeout = overrideFadeOutSpeed() == defaultSpeed() ? fadeOutSpeed() : overrideFadeOutSpeed(); + + /* If no fade out is needed, dismiss all the requested faders. + * Otherwise, set all the faders to fade out and let Universe dismiss them + * when done */ + if (fadeout == 0) + { + dismissAllFaders(); + } + else + { + if (tempoType() == Beats) + fadeout = beatsToTime(fadeout, timer->beatTimeDuration()); + + foreach (QSharedPointer fader, m_fadersMap) + { + if (!fader.isNull()) + fader->setFadeOut(true, fadeout); + } + } + + m_fadersMap.clear(); + + { + QMutexLocker algorithmLocker(&m_algorithmMutex); + checkEngineCreation(); + if (m_runAlgorithm != NULL) + m_runAlgorithm->postRun(); + } + + Function::postRun(timer, universes); +} + +bool HUEMatrix::roundCheckLocked() +{ + if (m_algorithm == NULL) + return false; + + bool advanced = m_stepHandler->checkNextStep(runOrder(), m_rgbColors[0], m_rgbColors[1], m_stepsCount); + if (advanced == false) + { + stop(FunctionParent::master()); + return false; + } + + // Update continuous phase based on current step index (prevents cumulative rounding errors) + // This is analogous to how EFX uses m_currentAngle for phase scaling + if (m_stepsCount > 0) + m_continuousPhase = double(m_stepHandler->currentStepIndex()) / double(m_stepsCount); + + m_roundTime.restart(); + + if (tempoType() == Beats) + roundElapsed(m_stepBeatDuration); + else + roundElapsed(duration()); + + return true; +} + +void HUEMatrix::roundCheck() +{ + QMutexLocker algorithmLocker(&m_algorithmMutex); + roundCheckLocked(); +} + +QSharedPointer HUEMatrix::getFader(Universe *universe) +{ + // get the universe Fader first. If doesn't exist, create it + if (universe == NULL) + return QSharedPointer(); + + QSharedPointer fader = m_fadersMap.value(universe->id(), QSharedPointer()); + if (fader.isNull()) + { + fader = universe->requestFader(Universe::blendModePriority(blendMode())); + fader->adjustIntensity(getAttributeValue(Intensity)); + fader->setBlendMode(blendMode()); + fader->setName(name()); + fader->setParentFunctionID(id()); + m_fadersMap[universe->id()] = fader; + } + + return fader; +} + +void HUEMatrix::updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime, uint fadeOutTime) +{ + fc.setStart(fc.current()); + fc.setTarget(value); + fc.setElapsed(0); + fc.setReady(false); + // fade in/out depends on target value + if (value == 0) + fc.setFadeTime(fadeOutTime); + else + fc.setFadeTime(fadeTime); +} + +void HUEMatrix::invalidatePixelPlan(quint32 id) +{ + Q_UNUSED(id) + // Conservative: any fixture / fixture-group change may affect the resolved + // channels. Marking dirty is cheap; the rebuild only happens on next tick. + m_pixelPlanDirty.storeRelease(1); +} + +void HUEMatrix::deferDeleteAlgorithm(RGBAlgorithm *algo) +{ + if (algo == nullptr) + return; + +#ifdef QT_QML_LIB + // Script-typed algorithms own QJSValue handles tied to the JSThread's + // QJSEngine. Even ignoring our async tasks, deleting them off-thread is + // unsafe. Queue the delete on the JSThread so it serializes after any + // pending precompute tasks (FIFO). + if (algo->type() == RGBAlgorithm::Script + && HUEScript::scheduleOnJSThread([algo]() { delete algo; })) + { + return; + } +#endif + delete algo; +} + +void HUEMatrix::kickAsyncRgbMap(RGBAlgorithm *algo, const QSize &algoSize, + uint stepColor, int step, quint32 generation) +{ +#ifdef QT_QML_LIB + if (algo == nullptr) + return; + + // Only Script algorithms benefit — others (Image, PlainColor, Text, Audio) + // run inline on this thread already and are fast. + if (algo->type() != RGBAlgorithm::Script) + return; + + // Throttle: at most one async task in flight per matrix. If a previous + // task hasn't been consumed yet, skip — the current frame's sync path is + // already producing usable output. + int expected = 0; + if (!m_precomputedInFlight.testAndSetAcquire(expected, 1)) + return; + + // Capture by value: algo pointer + immutable params + generation snapshot. + // We do NOT take m_algorithmMutex inside the JSThread task — that would + // deadlock with the synchronous rgbMap path (MasterTimer holds the mutex + // while BlockingQueuedConnection-waiting on JSThread). + // + // Lifetime safety: + // - algo pointer remains valid as long as any in-flight task could be + // using it: setAlgorithm() / ~RGBMatrix() use deferDeleteAlgorithm() + // which queues the delete on the JSThread *after* this task (FIFO). + // - `this` remains valid because ~RGBMatrix() spins on + // m_precomputedInFlight before destroying members. + QSize captureAlgoSize = algoSize; + int captureRotation = m_rotation; + int captureMirror = m_mirror; + MirrorBlend captureBlend = m_mirrorBlend; + QSize captureGroupSize; + if (m_group != nullptr) + captureGroupSize = m_group->size(); + bool captureUsesAudio = algo->usesAudio(); + + bool ok = HUEScript::scheduleOnJSThread( + [this, algo, captureAlgoSize, stepColor, step, generation, + captureRotation, captureMirror, captureBlend, captureGroupSize, + captureUsesAudio]() + { + // Re-check generation BEFORE we even call rgbMap. Cheap and lets + // us bail out if the matrix was reconfigured while we were queued. + if (m_currentGeneration.loadAcquire() != generation) + { + m_precomputedInFlight.storeRelease(0); + return; + } + + if (captureUsesAudio && !captureGroupSize.isEmpty()) + algo->setDisplaySize(captureGroupSize); + + RGBMap localMap; + algo->rgbMap(captureAlgoSize, stepColor, step, localMap); + + if ((captureRotation || captureMirror) && !captureGroupSize.isEmpty()) + applyTransforms(localMap, captureAlgoSize, captureGroupSize, + captureRotation, captureMirror, captureBlend); + + // Re-check generation AFTER computation. If a parameter changed + // mid-flight, drop the result rather than poison the cache. + if (m_currentGeneration.loadAcquire() != generation) + { + m_precomputedInFlight.storeRelease(0); + return; + } + + { + QMutexLocker pre(&m_precomputedMutex); + m_precomputedMap = std::move(localMap); + m_precomputedAlgoSize = captureAlgoSize; + m_precomputedStep = step; + m_precomputedColor = stepColor; + m_precomputedGeneration = generation; + m_precomputedAlgorithm = algo; + } + m_precomputedReady.storeRelease(1); + m_precomputedInFlight.storeRelease(0); + }); + + if (!ok) + m_precomputedInFlight.storeRelease(0); +#else + Q_UNUSED(algo) + Q_UNUSED(algoSize) + Q_UNUSED(stepColor) + Q_UNUSED(step) + Q_UNUSED(generation) +#endif +} + +void HUEMatrix::rebuildPixelPlan(const FixtureGroup *grp) +{ + m_pixelPlan.clear(); + m_pixelPlanGroup = grp; + m_pixelPlanDirty.storeRelease(0); + + if (grp == NULL) + return; + + Doc *d = doc(); + + QMapIterator it(grp->headsMap()); + while (it.hasNext()) + { + it.next(); + const QLCPoint &pt = it.key(); + const GroupHead &grpHead = it.value(); + + Fixture *fxi = d->fixture(grpHead.fxi); + if (fxi == NULL) + continue; + + QLCFixtureHead head = fxi->head(grpHead.head); + const quint32 absAddress = fxi->universeAddress(); + const quint32 fxID = grpHead.fxi; + const quint16 px = quint16(pt.x()); + const quint16 py = quint16(pt.y()); + + auto addEntry = [&](quint32 ch, PixelValueSource src) { + if (ch == QLCChannel::invalid()) + return; + PixelPlanEntry e; + e.x = px; + e.y = py; + e.universeIndex = quint32((absAddress + ch) / 512); + e.fixtureID = fxID; + e.channel = ch; + e.source = src; + m_pixelPlan.append(e); + }; + + if (m_controlMode == ControlModeRgb) + { + QVector chList = head.rgbChannels(); + if (chList.size() == 3) + { + addEntry(chList.at(0), VS_Red); + addEntry(chList.at(1), VS_Green); + addEntry(chList.at(2), VS_Blue); + } + else + { + chList = head.cmyChannels(); + if (chList.size() == 3) + { + addEntry(chList.at(0), VS_Cyan); + addEntry(chList.at(1), VS_Magenta); + addEntry(chList.at(2), VS_Yellow); + } + } + } + else if (m_controlMode == ControlModeRgbw || m_controlMode == ControlModeRgbwBrighter) + { + const bool subtractWhite = (m_controlMode == ControlModeRgbw); + quint32 rCh = head.channelNumber(QLCChannel::Red, QLCChannel::MSB); + quint32 gCh = head.channelNumber(QLCChannel::Green, QLCChannel::MSB); + quint32 bCh = head.channelNumber(QLCChannel::Blue, QLCChannel::MSB); + quint32 wCh = head.channelNumber(QLCChannel::White, QLCChannel::MSB); + + if (rCh != QLCChannel::invalid() && gCh != QLCChannel::invalid() && bCh != QLCChannel::invalid()) + { + addEntry(rCh, subtractWhite ? VS_RedSubW : VS_Red); + addEntry(gCh, subtractWhite ? VS_GreenSubW : VS_Green); + addEntry(bCh, subtractWhite ? VS_BlueSubW : VS_Blue); + + if (wCh != QLCChannel::invalid()) + addEntry(wCh, VS_White); + + if (m_dimmerControl) + { + quint32 masterDim = fxi->masterIntensityChannel(); + quint32 headDim = head.channelNumber(QLCChannel::Intensity, QLCChannel::MSB); + + if (masterDim != QLCChannel::invalid()) + addEntry(masterDim, VS_Grey); + + if (headDim != QLCChannel::invalid() && headDim != masterDim) + addEntry(headDim, VS_GreyOrFull); + } + } + } + else if (m_controlMode == ControlModeShutter) + { + QVector chList = head.shutterChannels(); + if (chList.size()) + addEntry(chList.first(), VS_Grey); + } + else if (m_controlMode == ControlModeDimmer || m_dimmerControl) + { + quint32 masterDim = fxi->masterIntensityChannel(); + quint32 headDim = head.channelNumber(QLCChannel::Intensity, QLCChannel::MSB); + + if (masterDim != QLCChannel::invalid()) + addEntry(masterDim, VS_Grey); + + if (headDim != QLCChannel::invalid() && headDim != masterDim) + addEntry(headDim, masterDim != QLCChannel::invalid() ? VS_GreyOrFull : VS_Grey); + } + else + { + quint32 ch = QLCChannel::invalid(); + if (m_controlMode == ControlModeWhite) + ch = head.channelNumber(QLCChannel::White, QLCChannel::MSB); + else if (m_controlMode == ControlModeAmber) + ch = head.channelNumber(QLCChannel::Amber, QLCChannel::MSB); + else if (m_controlMode == ControlModeUV) + ch = head.channelNumber(QLCChannel::UV, QLCChannel::MSB); + + addEntry(ch, VS_Grey); + } + } + + // Sort by (universeIndex, y, x, channel) so the per-tick loop can amortize + // the getFader() lookup across consecutive entries from the same universe, + // and benefit from per-pixel scratch reuse within a universe. + std::sort(m_pixelPlan.begin(), m_pixelPlan.end(), + [](const PixelPlanEntry &a, const PixelPlanEntry &b) { + if (a.universeIndex != b.universeIndex) return a.universeIndex < b.universeIndex; + if (a.y != b.y) return a.y < b.y; + if (a.x != b.x) return a.x < b.x; + return a.channel < b.channel; + }); +} + +void HUEMatrix::updateMapChannels(const RGBMap& map, const FixtureGroup *grp, QList universes, int beatDuration) +{ + uint fadeTime = (overrideFadeInSpeed() == defaultSpeed()) ? fadeInSpeed() : overrideFadeInSpeed(); + uint fadeOutTime = (overrideFadeOutSpeed() == defaultSpeed()) ? fadeOutSpeed() : overrideFadeOutSpeed(); + + if (tempoType() == Beats) + { + fadeTime = beatsToTime(fadeTime, beatDuration); + fadeOutTime = beatsToTime(fadeOutTime, beatDuration); + } + + // Rebuild the pre-resolved channel plan whenever it has been invalidated + // (fixture group / control mode / dimmer control / fixture address change). + if (m_pixelPlanDirty.loadAcquire() || m_pixelPlanGroup != grp) + rebuildPixelPlan(grp); + + if (m_pixelPlan.isEmpty()) + return; + + Doc *d = doc(); + const int mapH = map.count(); + const int universeCount = universes.size(); + + quint32 lastUni = UINT_MAX; + Universe *cachedUniverse = nullptr; + QSharedPointer cachedFader; + + quint16 lastX = 0xFFFF; + quint16 lastY = 0xFFFF; + bool pixelValid = false; + uchar r = 0, g = 0, b = 0, w = 0, grey = 0; + QColor cmyCol; + + for (int i = 0, n = m_pixelPlan.size(); i < n; ++i) + { + const PixelPlanEntry &e = m_pixelPlan.at(i); + + // Per-pixel scratch values. Recomputed only when (x, y) changes, + // which (after sorting) happens once per pixel within a universe. + if (e.x != lastX || e.y != lastY) + { + lastX = e.x; + lastY = e.y; + pixelValid = (int(e.y) < mapH && int(e.x) < map.at(e.y).count()); + if (pixelValid) + { + uint col = map.at(e.y).at(e.x); + r = uchar(qRed(col)); + g = uchar(qGreen(col)); + b = uchar(qBlue(col)); + w = qMin(r, qMin(g, b)); + grey = rgbToGrey(col); + cmyCol = QColor::fromRgb(col); + + if (m_brightness != 1.0) + { + r = uchar(qBound(0, int(r * m_brightness), 255)); + g = uchar(qBound(0, int(g * m_brightness), 255)); + b = uchar(qBound(0, int(b * m_brightness), 255)); + w = uchar(qBound(0, int(w * m_brightness), 255)); + grey = uchar(qBound(0, int(grey * m_brightness), 255)); + cmyCol = QColor::fromRgb(qBound(0, int(qRed(cmyCol.rgb()) * m_brightness), 255), + qBound(0, int(qGreen(cmyCol.rgb()) * m_brightness), 255), + qBound(0, int(qBlue(cmyCol.rgb()) * m_brightness), 255)); + } + } + } + + if (!pixelValid) + continue; + + // Universe / fader lookup is amortized across consecutive entries from + // the same universe thanks to the sort in rebuildPixelPlan(). + if (e.universeIndex != lastUni) + { + lastUni = e.universeIndex; + if (int(e.universeIndex) >= universeCount) + { + cachedUniverse = nullptr; + cachedFader.clear(); + } + else + { + cachedUniverse = universes.at(e.universeIndex); + cachedFader = getFader(cachedUniverse); + } + } + + if (cachedFader.isNull()) + continue; + + uchar value = 0; + switch (e.source) + { + case VS_Red: value = r; break; + case VS_Green: value = g; break; + case VS_Blue: value = b; break; + case VS_Cyan: value = uchar(cmyCol.cyan()); break; + case VS_Magenta: value = uchar(cmyCol.magenta()); break; + case VS_Yellow: value = uchar(cmyCol.yellow()); break; + case VS_White: value = w; break; + case VS_RedSubW: value = uchar(r - w); break; + case VS_GreenSubW: value = uchar(g - w); break; + case VS_BlueSubW: value = uchar(b - w); break; + case VS_Grey: value = grey; break; + case VS_GreyOrFull: value = grey == 0 ? 0 : 255; break; + } + + cachedFader->updateChannel(d, cachedUniverse, e.fixtureID, e.channel, + [this, value, fadeTime, fadeOutTime](FadeChannel &fc) + { + updateFaderValues(fc, value, fadeTime, fadeOutTime); + }); + } +} + + + +void HUEMatrix::setControlMode(RGBMatrix::ControlMode mode) +{ + m_controlMode = mode; + m_pixelPlanDirty.storeRelease(1); + // Control mode affects how rgbMap output is interpreted, but not the + // rgbMap output itself. Bump the generation defensively to avoid any + // edge case where the script could observe controlMode. + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + emit changed(id()); +} + +QString HUEMatrix::controlModeToString(RGBMatrix::ControlMode mode) +{ + switch (mode) + { + case RGBMatrix::ControlModeRgbw: + return QString(KXMLQLCRGBMatrixControlModeRgbw); + case RGBMatrix::ControlModeRgbwBrighter: + return QString(KXMLQLCRGBMatrixControlModeRgbwBrighter); + default: + return RGBMatrix::controlModeToString(mode); + } +} + +RGBMatrix::ControlMode HUEMatrix::stringToControlMode(QString mode) +{ + if (mode.compare(KXMLQLCRGBMatrixControlModeRgb, Qt::CaseInsensitive) == 0) + return ControlModeRgb; + else if (mode.compare(KXMLQLCRGBMatrixControlModeAmber, Qt::CaseInsensitive) == 0) + return ControlModeAmber; + else if (mode.compare(KXMLQLCRGBMatrixControlModeWhite, Qt::CaseInsensitive) == 0) + return ControlModeWhite; + else if (mode.compare(KXMLQLCRGBMatrixControlModeUV, Qt::CaseInsensitive) == 0) + return ControlModeUV; + else if (mode.compare(KXMLQLCRGBMatrixControlModeDimmer, Qt::CaseInsensitive) == 0) + return ControlModeDimmer; + else if (mode.compare(KXMLQLCRGBMatrixControlModeShutter, Qt::CaseInsensitive) == 0) + return ControlModeShutter; + else if (mode.compare(KXMLQLCRGBMatrixControlModeRgbw, Qt::CaseInsensitive) == 0) + return ControlModeRgbw; + else if (mode.compare(KXMLQLCRGBMatrixControlModeRgbwBrighter, Qt::CaseInsensitive) == 0) + return ControlModeRgbwBrighter; + + return ControlModeRgb; +} + +int HUEMatrix::rotation() const +{ + return m_rotation; +} + +void HUEMatrix::setRotation(int r) +{ + m_rotation = r & 3; + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + emit changed(id()); +} + +int HUEMatrix::mirror() const +{ + return m_mirror; +} + +void HUEMatrix::setMirror(int m) +{ + m_mirror = m & 3; + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + emit changed(id()); +} + +HUEMatrix::MirrorBlend HUEMatrix::mirrorBlend() const +{ + return m_mirrorBlend; +} + +void HUEMatrix::setMirrorBlend(MirrorBlend b) +{ + if (b < MirrorFlip || b > MirrorAdditive) + b = MirrorFlip; + m_mirrorBlend = b; + m_currentGeneration.fetchAndAddRelaxed(1); + m_precomputedReady.storeRelease(0); + emit changed(id()); +} + +qreal HUEMatrix::brightness() const +{ + return m_brightness; +} + +void HUEMatrix::setBrightness(qreal b) +{ + m_brightness = qMax(0.0, b); + emit changed(id()); +} + +QSize HUEMatrix::effectiveAlgorithmSize() const +{ + if (m_group == NULL) + return QSize(); + return effectiveAlgorithmSize(m_group); +} + +QSize HUEMatrix::effectiveAlgorithmSize(const FixtureGroup *grp) const +{ + QSize s = grp->size(); + if (m_rotation == 1 || m_rotation == 3) + s = QSize(s.height(), s.width()); + return s; +} + +QString HUEMatrix::mirrorBlendToString(MirrorBlend b) +{ + switch (b) + { + case MirrorMax: return QStringLiteral("Max"); + case MirrorAverage: return QStringLiteral("Average"); + case MirrorAdditive: return QStringLiteral("Additive"); + default: return QStringLiteral("Flip"); + } +} + +HUEMatrix::MirrorBlend HUEMatrix::stringToMirrorBlend(const QString &s) +{ + if (s.compare(QStringLiteral("Max"), Qt::CaseInsensitive) == 0) return MirrorMax; + if (s.compare(QStringLiteral("Average"), Qt::CaseInsensitive) == 0) return MirrorAverage; + if (s.compare(QStringLiteral("Additive"), Qt::CaseInsensitive) == 0) return MirrorAdditive; + return MirrorFlip; +} + +static inline uint blendPixels(uint a, uint b, HUEMatrix::MirrorBlend blend) +{ + uint ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF; + uint br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF; + uint r, g, bl; + + switch (blend) + { + case HUEMatrix::MirrorMax: + r = qMax(ar, br); g = qMax(ag, bg); bl = qMax(ab, bb); + break; + case HUEMatrix::MirrorAverage: + r = (ar + br) / 2; g = (ag + bg) / 2; bl = (ab + bb) / 2; + break; + case HUEMatrix::MirrorAdditive: + r = qMin(255u, ar + br); g = qMin(255u, ag + bg); bl = qMin(255u, ab + bb); + break; + default: // MirrorFlip — caller handles this case directly + return a; + } + return (r << 16) | (g << 8) | bl; +} + +void HUEMatrix::applyTransforms(RGBMap &map, const QSize & /* srcSize */, const QSize &dstSize, + int rotation, int mirror, MirrorBlend blend) +{ + int dw = dstSize.width(); + int dh = dstSize.height(); + + // Source dimensions from the actual map data + int sh = map.size(); + int sw = (sh > 0) ? map[0].size() : 0; + + // Step 1: Rotation — build a new map with destination dimensions + RGBMap rotated; + rotated.resize(dh); + for (int y = 0; y < dh; y++) + { + rotated[y].resize(dw); + rotated[y].fill(0); + } + + for (int dy = 0; dy < dh; dy++) + { + for (int dx = 0; dx < dw; dx++) + { + int sx, sy; + switch (rotation) + { + case 0: // 0°: identity + sx = dx; sy = dy; + break; + case 1: // 90° CW + sx = dy; sy = sh - 1 - dx; + break; + case 2: // 180° + sx = dw - 1 - dx; sy = dh - 1 - dy; + break; + case 3: // 270° CW + sx = sw - 1 - dy; sy = dx; + break; + default: + sx = dx; sy = dy; + break; + } + + if (sy >= 0 && sy < sh && sx >= 0 && sx < (int)map[sy].size()) + rotated[dy][dx] = map[sy][sx]; + } + } + + // Step 2: Mirror + if (mirror & 1) // Horizontal — mirror placed on vertical center line + { + if (blend == MirrorFlip) + { + // Pure flip: left half is source, copied to right half + for (int y = 0; y < dh; y++) + for (int x = 0; x < dw / 2; x++) + rotated[y][dw - 1 - x] = rotated[y][x]; + } + else + { + for (int y = 0; y < dh; y++) + { + for (int x = 0; x < dw / 2; x++) + { + int mx = dw - 1 - x; + uint merged = blendPixels(rotated[y][x], rotated[y][mx], blend); + rotated[y][x] = merged; + rotated[y][mx] = merged; + } + } + } + } + + if (mirror & 2) // Vertical — mirror placed on horizontal center line + { + if (blend == MirrorFlip) + { + // Pure flip: top half is source, copied to bottom half + for (int y = 0; y < dh / 2; y++) + for (int x = 0; x < dw; x++) + rotated[dh - 1 - y][x] = rotated[y][x]; + } + else + { + for (int y = 0; y < dh / 2; y++) + { + int my = dh - 1 - y; + for (int x = 0; x < dw; x++) + { + uint merged = blendPixels(rotated[y][x], rotated[my][x], blend); + rotated[y][x] = merged; + rotated[my][x] = merged; + } + } + } + } + + map = rotated; +} + +HUEMatrix::BeatEffect HUEMatrix::beatEffect() const +{ + return m_beatEffect; +} + +void HUEMatrix::setBeatEffect(BeatEffect e) +{ + m_beatEffect = e; +} + +HUEMatrix::BeatSelection HUEMatrix::beatSelection() const +{ + return m_beatSelection; +} + +void HUEMatrix::setBeatSelection(BeatSelection s) +{ + m_beatSelection = s; +} + +HUEMatrix::BeatOrientation HUEMatrix::beatOrientation() const +{ + return m_beatOrientation; +} + +void HUEMatrix::setBeatOrientation(BeatOrientation o) +{ + m_beatOrientation = o; +} + +QString HUEMatrix::beatEffectToString(BeatEffect e) +{ + switch (e) + { + case BeatEffectMirror: return QStringLiteral("Mirror"); + case BeatEffectColorInvert: return QStringLiteral("ColorInvert"); + case BeatEffectBlackout: return QStringLiteral("Blackout"); + case BeatEffectWhiteout: return QStringLiteral("Whiteout"); + default: return QStringLiteral("Off"); + } +} + +HUEMatrix::BeatEffect HUEMatrix::stringToBeatEffect(const QString &s) +{ + if (s.compare(QStringLiteral("Mirror"), Qt::CaseInsensitive) == 0) return BeatEffectMirror; + if (s.compare(QStringLiteral("ColorInvert"), Qt::CaseInsensitive) == 0) return BeatEffectColorInvert; + if (s.compare(QStringLiteral("Blackout"), Qt::CaseInsensitive) == 0) return BeatEffectBlackout; + if (s.compare(QStringLiteral("Whiteout"), Qt::CaseInsensitive) == 0) return BeatEffectWhiteout; + return BeatEffectOff; +} + +QString HUEMatrix::beatSelectionToString(BeatSelection s) +{ + switch (s) + { + case BeatSelWalk: return QStringLiteral("Walk"); + case BeatSelRandom: return QStringLiteral("Random"); + default: return QStringLiteral("AllOnDownbeat"); + } +} + +HUEMatrix::BeatSelection HUEMatrix::stringToBeatSelection(const QString &s) +{ + if (s.compare(QStringLiteral("Walk"), Qt::CaseInsensitive) == 0) return BeatSelWalk; + if (s.compare(QStringLiteral("Random"), Qt::CaseInsensitive) == 0) return BeatSelRandom; + return BeatSelAllOnDownbeat; +} + +QString HUEMatrix::beatOrientationToString(BeatOrientation o) +{ + switch (o) + { + case BeatOrientColumns: return QStringLiteral("Columns"); + default: return QStringLiteral("Rows"); + } +} + +HUEMatrix::BeatOrientation HUEMatrix::stringToBeatOrientation(const QString &s) +{ + if (s.compare(QStringLiteral("Columns"), Qt::CaseInsensitive) == 0) return BeatOrientColumns; + return BeatOrientRows; +} + +void HUEMatrix::segmentRange(int segment, int total, int segmentsCount, int &start, int &end) +{ + if (segmentsCount < 1) segmentsCount = 1; + int base = total / segmentsCount; + int remainder = total % segmentsCount; + start = segment * base + qMin(segment, remainder); + end = start + base + (segment < remainder ? 1 : 0); +} + +void HUEMatrix::applyBeatTransform(RGBMap &map, int currentBeat, int beatsPerBar) +{ + if (m_beatEffect == BeatEffectOff) + return; + + int rows = map.size(); + if (rows == 0) return; + int cols = map[0].size(); + if (cols == 0) return; + + int total = (m_beatOrientation == BeatOrientRows) ? rows : cols; + if (beatsPerBar < 1) beatsPerBar = 1; + + // Determine affected segments + QVector affected; + if (m_beatSelection == BeatSelAllOnDownbeat) + { + if (currentBeat != beatsPerBar - 1) return; + affected.reserve(beatsPerBar); + for (int i = 0; i < beatsPerBar; i++) affected.append(i); + } + else if (m_beatSelection == BeatSelWalk) + { + affected = {currentBeat % beatsPerBar}; + } + else // BeatSelRandom + { + affected = {m_randomSegment % beatsPerBar}; + } + + for (int seg : affected) + { + int start, end; + segmentRange(seg, total, beatsPerBar, start, end); + + switch (m_beatEffect) + { + case BeatEffectMirror: + if (m_beatOrientation == BeatOrientRows) + { + for (int y = start; y < end; y++) + std::reverse(map[y].begin(), map[y].end()); + } + else + { + for (int y = 0; y < rows; y++) + std::reverse(map[y].begin() + start, map[y].begin() + end); + } + break; + + case BeatEffectColorInvert: + if (m_beatOrientation == BeatOrientRows) + { + for (int y = start; y < end; y++) + for (int x = 0; x < cols; x++) + map[y][x] ^= 0x00FFFFFF; + } + else + { + for (int y = 0; y < rows; y++) + for (int x = start; x < end; x++) + map[y][x] ^= 0x00FFFFFF; + } + break; + + case BeatEffectBlackout: + if (m_beatOrientation == BeatOrientRows) + { + for (int y = start; y < end; y++) + map[y].fill(0); + } + else + { + for (int y = 0; y < rows; y++) + for (int x = start; x < end; x++) + map[y][x] = 0; + } + break; + + case BeatEffectWhiteout: + if (m_beatOrientation == BeatOrientRows) + { + for (int y = start; y < end; y++) + map[y].fill(0x00FFFFFF); + } + else + { + for (int y = 0; y < rows; y++) + for (int x = start; x < end; x++) + map[y][x] = 0x00FFFFFF; + } + break; + + default: + break; + } + } +} diff --git a/engine/src/huematrix.h b/engine/src/huematrix.h new file mode 100644 index 0000000000..9e6ecb7db6 --- /dev/null +++ b/engine/src/huematrix.h @@ -0,0 +1,422 @@ +/* + Q Light Controller Plus + huematrix.h + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUEMATRIX_H +#define HUEMATRIX_H + +#include +#include +#include + +#include "rgbmatrix.h" + +/** @addtogroup engine Engine + * @{ + */ + +/** + * A matrix function that inherits everything RGBMatrix does and adds the + * fork-specific rendering pipeline on top of it: + * + * - HSV script contract (via HUEScript) plus the HSV script library + * - rotation / mirror with selectable blend + * - beat-driven transforms + * - a post-render brightness multiplier + * - the RGBW and RGBW-brighter control modes + * - a pre-resolved pixel plan and async rgbMap pre-computation + * + * RGBMatrix itself is kept byte-identical to upstream, so write() is fully + * overridden here rather than hooked into the base implementation. Roughly + * 35 lines of the write() preamble are therefore duplicated by design. + */ +class HUEMatrix : public RGBMatrix +{ + Q_OBJECT + Q_DISABLE_COPY(HUEMatrix) + + /********************************************************************* + * Initialization + *********************************************************************/ +public: + HUEMatrix(Doc *doc); + ~HUEMatrix(); + + /** @reimp */ + QIcon getIcon() const override; + + /** + * Names of every algorithm a HUEMatrix can run: the built-in ones, the + * upstream RGB scripts and the HSV-contract HUE scripts. + */ + static QStringList availableAlgorithms(Doc *doc); + + /** Instantiate an algorithm by name. Returns NULL when $name is unknown. */ + static RGBAlgorithm *createAlgorithm(Doc *doc, const QString &name); + + /** Load an algorithm from XML, resolving scripts through the HUE cache. */ + static RGBAlgorithm *algorithmLoader(Doc *doc, QXmlStreamReader &root); + + /** @reimp */ + void setTotalDuration(quint32 msec) override; + + /** @reimp */ + quint32 totalDuration() override; + + /** @reimp */ + void setDimmerControl(bool dimmerControl) override; + + /********************************************************************* + * Copying + *********************************************************************/ +public: + /** @reimp */ + Function *createCopy(Doc *doc, bool addToDoc = true) override; + + /** @reimp */ + bool copyFrom(const Function *function) override; + + /********************************************************************* + * Fixture Group / Algorithm + *********************************************************************/ +public: + /** Set the fixture group and invalidate the pixel plan / precomputed map */ + void setFixtureGroup(quint32 id) override; + + /** Set the algorithm and invalidate the pixel plan / precomputed map */ + void setAlgorithm(RGBAlgorithm *algo) override; + + /** @reimp */ + void previewMap(int step, RGBMatrixStep *handler) override; + +protected: + /** @reimp */ + int algorithmStepsCount() override; + +public: + + /********************************************************************* + * Load & Save + *********************************************************************/ +public: + /** @reimp */ + bool loadXML(QXmlStreamReader &root) override; + + /** @reimp */ + bool saveXML(QXmlStreamWriter *doc) const override; + + /********************************************************************* + * Running + *********************************************************************/ +public: + /** @reimp */ + void preRun(MasterTimer *timer) override; + + /** @reimp */ + void write(MasterTimer *timer, QList universes) override; + + /** @reimp */ + void postRun(MasterTimer *timer, QList universes) override; + +protected: + /** @reimp */ + void roundCheck() override; + + /** Same as roundCheck but assumes m_algorithmMutex is already held. + * Returns true if the step actually advanced, false if the function + * stopped (SingleShot end) or algorithm is null. */ + bool roundCheckLocked(); + + /** Advance the elapsed time/beat counters by one tick and, when the step + * is due, move to the next one. Writes the pre-increment elapsed time to + * $prevElapsed so the caller can detect the first tick of a step. + * Returns true if the step actually advanced. + * Assumes m_algorithmMutex is held. */ + bool advanceStep(MasterTimer *timer, quint32 &prevElapsed); + + /** Track the position within the musical bar (m_currentBeat) and latch a + * new random segment on beat changes. Returns the number of beats per bar + * to use for the beat transform. No-op when the beat effect is off. + * Assumes m_algorithmMutex is held. */ + int updateBeatPhase(MasterTimer *timer); + + /** Try to move a matching pre-computed frame into m_stepHandler->m_map. + * A frame only matches if it was produced for the same generation, + * algorithm, step, colour and size. A non-matching frame is discarded. + * Returns true if the map was filled from the pre-computed frame. */ + bool consumePrecomputedMap(const QSize &algoSize, uint stepColor, + int stepIndex, quint32 generation); + + QSharedPointer getFader(Universe *universe); + void updateFaderValues(FadeChannel &fc, uchar value, uint fadeTime, uint fadeOutTime); + + /** Update FadeChannels when $map has changed since last time */ + void updateMapChannels(const RGBMap &map, const FixtureGroup *grp, + QList universes, int beatDuration); + + /********************************************************************* + * Pixel plan + *********************************************************************/ +protected: + /** Pre-computed channel writes for one fixture-head pixel. + * See rebuildPixelPlan() for invalidation triggers. */ + enum PixelValueSource : quint8 + { + VS_Red, VS_Green, VS_Blue, + VS_Cyan, VS_Magenta, VS_Yellow, + VS_White, ///< min(R,G,B) + VS_RedSubW, VS_GreenSubW, VS_BlueSubW, + VS_Grey, ///< rgbToGrey(col) + VS_GreyOrFull ///< 0 if grey == 0 else 255 + }; + + struct PixelPlanEntry + { + quint16 x; + quint16 y; + quint32 universeIndex; + quint32 fixtureID; + quint32 channel; ///< channel number relative to fixture + PixelValueSource source; + }; + + /** Flat, pre-resolved list of channel writes for the current fixture group + * + control mode. Avoids per-tick fixture/head/channel lookups. */ + QVector m_pixelPlan; + + /** Cached pointer used to detect that the resolved FixtureGroup changed + * (in addition to the dirty flag). */ + const FixtureGroup *m_pixelPlanGroup = nullptr; + + /** When non-zero, m_pixelPlan must be rebuilt before use. Marked from + * fixture / fixture-group / control-mode changes. Atomic so that signals + * from the main thread can flip it while the MasterTimer thread reads. */ + QAtomicInteger m_pixelPlanDirty; + + /** Rebuild m_pixelPlan from $grp using the current control mode and + * dimmer-control flag. Caller must hold m_algorithmMutex. */ + void rebuildPixelPlan(const FixtureGroup *grp); + + /********************************************************************* + * Async pre-computation of next frame's RGBMap + * + * The JS rgbMap() call dominates the MasterTimer budget. We pre-compute + * the *next* frame's pixel data on the shared JSThread at the END of the + * current write(), so that by the time the MasterTimer reaches us again + * the result is already waiting in m_precomputedMap. + * + * Validity is gated by: + * - m_precomputedReady flag (atomic): producer/consumer handshake. + * - Generation counter: invalidated on algorithm/group/control mode + * changes that would render a precomputed map unsafe or wrong. + * - Per-frame match: step index, step color, algorithm size and the + * algorithm pointer must equal what the consuming write() needs. + * + * If any check fails, we fall back to the synchronous rgbMap() path — + * correctness is preserved, only the perf win is missed. + *********************************************************************/ +protected: + /** Upper bound, in milliseconds, on how long ~HUEMatrix() waits for an + * in-flight async precompute to drain. Exceeding it means the JS thread + * went away before the queued job ran, so the flag will never clear. */ + static const int precomputeDrainTimeoutMs = 2000; + + /** Pre-computed pixel map for the next frame. Protected by + * m_precomputedMutex. */ + RGBMap m_precomputedMap; + + /** Guards the m_precomputed* fields below. Cheap (uncontended) — the + * only writer is the JSThread async task; the only reader is the + * MasterTimer thread once per tick. */ + QMutex m_precomputedMutex; + + /** 0 = no precomputed frame available, 1 = ready to consume. */ + QAtomicInteger m_precomputedReady; + + /** Set to 1 while a JSThread async task is queued or running for this + * matrix. Used to throttle (one outstanding task at a time) and to let + * the destructor wait for completion before tearing down. */ + QAtomicInteger m_precomputedInFlight; + + /** Bumped on any change that invalidates an in-flight or stored + * precomputed map (setAlgorithm, setFixtureGroup, setControlMode, + * preRun). Color changes do NOT bump — they are validated per-frame + * via m_precomputedColor instead, so rapid color attribute fades + * don't thrash the cache. */ + QAtomicInteger m_currentGeneration; + + /** Generation snapshot taken at kick-off; compared against + * m_currentGeneration before storing/consuming the precomputed map. */ + quint32 m_precomputedGeneration; + + /** Step index, step color and algorithm size the precomputed map was + * rendered for. The consuming write() requires these to match its + * intended frame parameters. */ + int m_precomputedStep; + uint m_precomputedColor; + QSize m_precomputedAlgoSize; + + /** Algorithm pointer the precomputed map was rendered against. + * Compared on consume to detect setAlgorithm() races that the + * generation counter alone might miss. */ + RGBAlgorithm *m_precomputedAlgorithm; + + /** Kick a non-blocking pre-computation of the next frame's rgbMap on + * the JSThread. Caller must hold m_algorithmMutex. No-op if the + * algorithm isn't a Script (other algorithms are fast and don't + * benefit) or if a task is already in flight. */ + void kickAsyncRgbMap(RGBAlgorithm *algo, const QSize &algoSize, + uint stepColor, int step, quint32 generation); + + /** Delete an algorithm pointer safely. Script-typed algorithms are + * deleted via the JSThread to drain any pending precompute tasks + * (FIFO ordering) before destruction. Other types are deleted inline. */ + static void deferDeleteAlgorithm(RGBAlgorithm *algo); + +protected slots: + /** Slot connected to Doc fixture/group change signals. Marks the + * PixelPlan as dirty so it will be rebuilt on the next tick. */ + void invalidatePixelPlan(quint32 id = 0); + + /************************************************************************* + * Control Mode + *************************************************************************/ +public: + /** Set the control mode, invalidating the pixel plan */ + void setControlMode(RGBMatrix::ControlMode mode) override; + + /** Return a control mode from a string, including the RGBW modes */ + static RGBMatrix::ControlMode stringToControlMode(QString mode); + + /** Return a string from a control mode, including the RGBW modes */ + static QString controlModeToString(RGBMatrix::ControlMode mode); + + /************************************************************************* + * Rotation & Mirror + *************************************************************************/ +public: + /** Mirror blend algorithms */ + enum MirrorBlend + { + MirrorFlip = 0, ///< Pure reflection (copy one half to other) + MirrorMax, ///< qMax per channel (LedFX-style bloom) + MirrorAverage, ///< (a + b) / 2 per channel + MirrorAdditive ///< min(255, a + b) per channel + }; + + /** Get/Set rotation: 0=0°, 1=90°CW, 2=180°, 3=270°CW */ + int rotation() const; + void setRotation(int r); + + /** Get/Set mirror: 0=off, 1=horizontal, 2=vertical, 3=both */ + int mirror() const; + void setMirror(int m); + + /** Get/Set mirror blend algorithm */ + MirrorBlend mirrorBlend() const; + void setMirrorBlend(MirrorBlend b); + + /** Return the effective algorithm size, accounting for 90/270 rotation */ + QSize effectiveAlgorithmSize() const; + + static QString mirrorBlendToString(MirrorBlend b); + static MirrorBlend stringToMirrorBlend(const QString &s); + + /** Apply rotation and mirror transforms to a rendered RGB map. + * srcSize = the size the algorithm was given (may be swapped for 90/270). + * dstSize = the physical fixture group size. */ + static void applyTransforms(RGBMap &map, const QSize &srcSize, const QSize &dstSize, + int rotation, int mirror, MirrorBlend blend); + +protected: + /** Return effective algorithm size for a given fixture group */ + QSize effectiveAlgorithmSize(const FixtureGroup *grp) const; + + int m_rotation; + int m_mirror; + MirrorBlend m_mirrorBlend; + + /************************************************************************* + * Brightness multiplier + *************************************************************************/ +public: + /** Get/Set the post-render brightness multiplier (>= 0.0, 1.0 = unity) */ + qreal brightness() const; + void setBrightness(qreal b); + +protected: + qreal m_brightness; + + /************************************************************************* + * Beat Transform + *************************************************************************/ +public: + enum BeatEffect + { + BeatEffectOff = 0, + BeatEffectMirror, + BeatEffectColorInvert, + BeatEffectBlackout, + BeatEffectWhiteout + }; + + enum BeatSelection + { + BeatSelAllOnDownbeat = 0, + BeatSelWalk, + BeatSelRandom + }; + + enum BeatOrientation + { + BeatOrientRows = 0, + BeatOrientColumns + }; + + BeatEffect beatEffect() const; + void setBeatEffect(BeatEffect e); + + BeatSelection beatSelection() const; + void setBeatSelection(BeatSelection s); + + BeatOrientation beatOrientation() const; + void setBeatOrientation(BeatOrientation o); + + static QString beatEffectToString(BeatEffect e); + static BeatEffect stringToBeatEffect(const QString &s); + static QString beatSelectionToString(BeatSelection s); + static BeatSelection stringToBeatSelection(const QString &s); + static QString beatOrientationToString(BeatOrientation o); + static BeatOrientation stringToBeatOrientation(const QString &s); + +protected: + void applyBeatTransform(RGBMap &map, int currentBeat, int beatsPerBar); + + static void segmentRange(int segment, int total, int segmentsCount, int &start, int &end); + + BeatEffect m_beatEffect; + BeatSelection m_beatSelection; + BeatOrientation m_beatOrientation; + int m_currentBeat; + int m_lastBeat; + int m_randomSegment; +}; + +/** @} */ + +#endif // HUEMATRIX_H diff --git a/engine/src/huescript.cpp b/engine/src/huescript.cpp new file mode 100644 index 0000000000..bc028a8337 --- /dev/null +++ b/engine/src/huescript.cpp @@ -0,0 +1,600 @@ +/* + Q Light Controller Plus + huescript.cpp + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "jsthread_p.h" +#include "huescript.h" +#include "huescriptscache.h" +#include "huematrix.h" + +#include "audiochannel.h" +#include "audiocapture.h" +#include "audioprofile.h" +#include "audiosnapshot.h" +#include "mastertimer.h" +#include "doc.h" + +#include "huecolor.h" + +namespace +{ + static constexpr int kAudioApiVersion = 5; + static constexpr int kBeatsPerBar = 4; + static constexpr double kPi = 3.14159265358979323846; + + RGBMatrix *owningMatrix(Doc *doc, const RGBScript *script) + { + if (doc == NULL || script == NULL) + return NULL; + + foreach (Function *function, doc->functionsByType(Function::HUEMatrixType)) + { + RGBMatrix *matrix = qobject_cast (function); + if (matrix != NULL && matrix->algorithm() == script) + return matrix; + } + + return NULL; + } + + bool onsetFiredAt(const AudioSnapshot &snap, int methodIndex) + { + switch (methodIndex) + { + case 0: return snap.onsets.energy; + case 1: return snap.onsets.hfc; + case 2: return snap.onsets.complex_; + case 3: return snap.onsets.phase; + case 4: return snap.onsets.wphase; + case 5: return snap.onsets.specdiff; + case 6: return snap.onsets.kl; + case 7: return snap.onsets.mkl; + case 8: return snap.onsets.specflux; + default: return false; + } + } +} + +/**************************************************************************** + * Initialization + ****************************************************************************/ + +HUEScript::HUEScript(Doc *doc) + : RGBScript(doc) + , m_hsvContract(true) + , m_usesAudio(false) + , m_audioInput(NULL) + , m_audioRegistered(false) + , m_hsvContractValidated(false) +{ +} + +HUEScript::HUEScript(const HUEScript &s) + : RGBScript(s) + , m_hsvContract(s.m_hsvContract) + , m_usesAudio(false) + , m_audioInput(NULL) + , m_audioRegistered(false) + , m_hsvContractValidated(false) +{ + // RGBScript's copy constructor already evaluated the contents through the + // base implementation; re-read the audio metadata for this instance. + parseAudioMetadata(); +} + +HUEScript::~HUEScript() +{ + teardownAudioCapture(); +} + +RGBAlgorithm *HUEScript::clone() const +{ + HUEScript *script = new HUEScript(*this); + return static_cast (script); +} + +void HUEScript::setHsvContract(bool hsv) +{ + m_hsvContract = hsv; +} + +bool HUEScript::hsvContract() const +{ + return m_hsvContract; +} + +/**************************************************************************** + * Load & Evaluation + ****************************************************************************/ + +void HUEScript::loadHsvShims() +{ + static bool s_loaded = false; + + + RGBScript::initEngine(); + + if (s_loaded || s_jsThread == NULL || s_jsThread->engine == NULL) + return; + + s_loaded = true; + + const QStringList shimNames = { QStringLiteral("hsvutil.js") }; + + for (const QString &shimName : shimNames) + { + QString shimPath; + foreach (QString dirPath, HUEScriptsCache::hsvScriptDirectories()) + { + QString candidate = QDir(dirPath).filePath(shimName); + if (QFile::exists(candidate)) + { + shimPath = candidate; + break; + } + } + + QFile shimFile(shimPath); + if (shimPath.isEmpty() || shimFile.open(QIODevice::ReadOnly) == false) + { + qWarning() << "[HUEScript] shim not found:" << shimName; + continue; + } + + QString shimContents = QTextStream(&shimFile).readAll(); + shimFile.close(); + + auto runShim = [shimContents, shimPath]{ + QJSValue result = s_jsThread->engine->evaluate(shimContents, shimPath); + if (result.isError()) + RGBScript::displayError(result, shimPath); + }; + + // evaluate() may already have marshalled us onto the JS thread; a + // blocking queued call to our own thread is a deadlock. + if (QThread::currentThread() == s_jsThread) + runShim(); + else + QMetaObject::invokeMethod(s_jsThread->engine, runShim, Qt::BlockingQueuedConnection); + } +} + +bool HUEScript::evaluate() +{ + // Bring the JS thread up before the guard, so the very first evaluate() + // is marshalled too rather than running inline on the caller's thread. + RGBScript::initEngine(); + + // Every QJSValue this method reaches belongs to the JS engine thread. + // Same guard shape as RGBScript::evaluate(). + if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) + { + bool retVal = false; + QMetaObject::invokeMethod(s_jsThread->engine, [this]{ return evaluate(); }, + Qt::BlockingQueuedConnection, &retVal); + return retVal; + } + + loadHsvShims(); + + m_usesAudio = false; + m_audioInputCategories.clear(); + m_hsvContractValidated = false; + + if (RGBScript::evaluate() == false) + return false; + + parseAudioMetadata(); + return true; +} + +void HUEScript::parseAudioMetadata() +{ + // Reads m_script properties, so it must run on the JS engine thread. + // Reachable from the copy constructor, which has no guard of its own. + if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) + { + QMetaObject::invokeMethod(s_jsThread->engine, [this]{ parseAudioMetadata(); }, + Qt::BlockingQueuedConnection); + return; + } + + m_usesAudio = false; + m_audioInputCategories.clear(); + + if (m_script.isObject() == false) + return; + + QJSValue usesAudioVal = m_script.property(QStringLiteral("usesAudio")); + m_usesAudio = (!usesAudioVal.isUndefined() && usesAudioVal.toBool()); + if (m_usesAudio == false) + return; + + QJSValue inputsVal = m_script.property(QStringLiteral("audioInputs")); + if (inputsVal.isArray()) + { + const int len = inputsVal.property(QStringLiteral("length")).toInt(); + for (int i = 0; i < len; ++i) + { + QString cat = inputsVal.property(quint32(i)).toString(); + if (!cat.isEmpty() && !m_audioInputCategories.contains(cat)) + m_audioInputCategories.append(cat); + } + return; + } + + // Auto-detect from source: extract top-level audio.X references + static const QRegularExpression rx(QStringLiteral("\\baudio\\.(\\w+)")); + QRegularExpressionMatchIterator it = rx.globalMatch(m_contents); + QSet seen; + while (it.hasNext()) + { + QString cat = it.next().captured(1); + if (cat != QStringLiteral("timing") && !seen.contains(cat)) + { + seen.insert(cat); + m_audioInputCategories.append(cat); + } + } + m_audioInputCategories.sort(); +} + +bool HUEScript::scheduleOnJSThread(std::function fn) +{ + if (s_jsThread == NULL || s_jsThread->engine == NULL) + return false; + + QMetaObject::invokeMethod(s_jsThread->engine, std::move(fn), Qt::QueuedConnection); + return true; +} + +/**************************************************************************** + * RGBAlgorithm API + ****************************************************************************/ + +void HUEScript::rgbMapSetColors(const QVector &colors) +{ + // Same guard shape as RGBScript::rgbMapSetColors(). + if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) + { + QMetaObject::invokeMethod(s_jsThread->engine, [this, colors]{ rgbMapSetColors(colors); }, + Qt::QueuedConnection); + return; + } + + if (m_hsvContract == false) + { + RGBScript::rgbMapSetColors(colors); + return; + } + + if (m_apiVersion <= 2 || m_rgbMapSetColors.isCallable() == false) + return; + + int accColors = acceptColors(); + int rawColorCount = colors.count(); + + // HSV contract: scripts receive an array of {h,s,v} objects, never + // packed RGB integers. + QJSEngine *engine = s_jsThread->engine; + QJSValue jsRawColors = engine->newArray(accColors); + for (int i = 0; i < rawColorCount && i < accColors; i++) + jsRawColors.setProperty(i, HUEColor::hsvToJs(engine, HUEColor::rgbToHsv(colors.at(i) & 0xFFFFFFu))); + + QJSValueList args; + args << jsRawColors; + + QJSValue value = m_rgbMapSetColors.call(args); + if (value.isError()) + displayError(value, m_fileName); +} + +void HUEScript::rgbMap(const QSize &size, uint rgb, int step, RGBMap &map) +{ + // Same guard shape as RGBScript::rgbMap(). This is also what protects the + // live DMX path, where HUEMatrix::write() calls us on the MasterTimer + // thread whenever the async precompute misses. + if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) + { + QMetaObject::invokeMethod(s_jsThread->engine, + [this, size, rgb, step, &map]{ rgbMap(size, rgb, step, map); }, + Qt::BlockingQueuedConnection); + return; + } + + if (m_hsvContract == false) + { + // Upstream contract: nested packed-uint arrays, packed uint color arg. + RGBScript::rgbMap(size, rgb, step, map); + return; + } + + if (m_rgbMap.isUndefined() == true) + return; + + RGBMatrix *matrix = owningMatrix(doc(), this); + + injectColors(rgb, matrix); + + if (m_usesAudio) + setupAudioCapture(); + + QJSEngine *engine = s_jsThread->engine; + QJSValueList args; + // 3rd argument is the primary color as {h,s,v}; the signature + // rgbMap(width, height, color, step[, audio]) stays positional. + args << size.width() << size.height() + << HUEColor::hsvToJs(engine, HUEColor::rgbToHsv(rgb & 0xFFFFFFu)) << step; + + if (m_usesAudio) + args << buildAudioDataObject(); + + QJSValue yarray(m_rgbMap.call(args)); + if (yarray.isError()) + { + displayError(yarray, m_fileName); + return; + } + + const int width = size.width(); + const int height = size.height(); + if (width <= 0 || height <= 0) + return; + + const int expectedHsvBytes = width * height * 3 * 4; // 3 floats/pixel + + // Full validation on first frame; fast-path on subsequent frames. + if (!m_hsvContractValidated) + { + QJSValue bpeProp = yarray.property(QStringLiteral("BYTES_PER_ELEMENT")); + if (!bpeProp.isNumber()) + { + qWarning() << "HUEScript" << m_fileName + << "rgbMap() did not return a TypedArray. HSV contract" + << "requires a flat Float32Array of length width*height*3."; + return; + } + + QJSValue ctorName = yarray.property(QStringLiteral("constructor")) + .property(QStringLiteral("name")); + if (ctorName.toString() != QStringLiteral("Float32Array")) + { + qWarning() << "HUEScript" << m_fileName + << "returned" << ctorName.toString() + << "but only Float32Array (HSV) is supported."; + return; + } + + if (bpeProp.toInt() != 4) + { + qWarning() << "HUEScript" << m_fileName + << "unexpected BYTES_PER_ELEMENT:" << bpeProp.toInt(); + return; + } + + m_hsvContractValidated = true; + } + + const int byteLength = yarray.property(QStringLiteral("byteLength")).toInt(); + if (byteLength != expectedHsvBytes) + { + qWarning() << "HUEScript" << m_fileName + << "TypedArray size mismatch: byteLength=" << byteLength + << "(expected" << expectedHsvBytes << ")"; + return; + } + + QJSValue bufProp = yarray.property(QStringLiteral("buffer")); + if (!bufProp.isObject()) + { + qWarning() << "HUEScript" << m_fileName + << "TypedArray has no underlying ArrayBuffer"; + return; + } + + QByteArray bytes = bufProp.toVariant().toByteArray(); + if (bytes.size() != expectedHsvBytes) + { + qWarning() << "HUEScript" << m_fileName + << "ArrayBuffer extraction size mismatch: got" << bytes.size() + << "expected" << expectedHsvBytes; + return; + } + + map.resize(height); + const float *src = reinterpret_cast (bytes.constData()); + for (int y = 0; y < height; ++y) + { + map[y].resize(width); + const float *row = src + (y * width * 3); + for (int x = 0; x < width; ++x) + { + int i = x * 3; + map[y][x] = HUEColor::hsvToRgb(row[i], row[i + 1], row[i + 2]); + } + } +} + +bool HUEScript::usesAudio() const +{ + return m_usesAudio; +} + +QStringList HUEScript::audioInputCategories() const +{ + return m_audioInputCategories; +} + +void HUEScript::setDisplaySize(const QSize &size) +{ + if (s_jsThread != NULL && QThread::currentThread() != s_jsThread) + { + QMetaObject::invokeMethod(s_jsThread->engine, [this, size]{ setDisplaySize(size); }, + Qt::BlockingQueuedConnection); + return; + } + + if (!m_script.isObject()) + return; + + m_script.setProperty(QStringLiteral("displayWidth"), size.width()); + m_script.setProperty(QStringLiteral("displayHeight"), size.height()); +} + +void HUEScript::postRun() +{ + teardownAudioCapture(); +} + +/**************************************************************************** + * Audio support + ****************************************************************************/ + +void HUEScript::setupAudioCapture() +{ + if (doc() == NULL) + return; + + QSharedPointer capture = doc()->audioInputCapture(); + if (capture.isNull()) + return; + + if (m_audioInput != NULL && capture.data() == m_audioInput) + return; // Already connected to this capture + + teardownAudioCapture(); + m_audioInput = capture.data(); + + // The audio object is sourced exclusively from the AudioProfile's + // AudioChannel snapshot inside buildAudioDataObject(); we only need the + // capture thread to be running. + m_audioInput->registerBandsNumber(1); + m_audioRegistered = true; +} + +void HUEScript::teardownAudioCapture() +{ + if (m_audioInput != NULL) + { + if (m_audioRegistered) + { + m_audioInput->unregisterBandsNumber(1); + m_audioRegistered = false; + } + m_audioInput = NULL; + } +} + +void HUEScript::injectColors(uint rgb, RGBMatrix *matrix) +{ + QJSEngine *engine = s_jsThread->engine; + if (engine == NULL || m_script.isUndefined()) + return; + + int numColors = acceptColors(); + if (numColors <= 0) + { + m_script.setProperty(QStringLiteral("colors"), engine->newArray(0)); + return; + } + + // Convert user-picked color stops to HSV. Invalid/missing slots get the + // step color fallback so algo.colors always has exactly numColors entries. + HUEColor::Hsv fallback = HUEColor::rgbToHsv(rgb & 0xFFFFFFu); + QVector cols; + if (matrix != NULL) + cols = matrix->getColors(); + + bool anyUserSet = false; + QJSValue colorsArr = engine->newArray(quint32(numColors)); + for (int i = 0; i < numColors; ++i) + { + bool valid = (i < cols.size() && cols.at(i).isValid()); + if (valid) + anyUserSet = true; + HUEColor::Hsv hsv = valid ? HUEColor::rgbToHsv(cols.at(i).rgb() & 0xFFFFFFu) : fallback; + colorsArr.setProperty(quint32(i), HUEColor::hsvToJs(engine, hsv)); + } + + m_script.setProperty(QStringLiteral("colors"), colorsArr); + m_script.setProperty(QStringLiteral("hasUserColors"), QJSValue(anyUserSet)); +} + +QJSValue HUEScript::buildAudioDataObject() +{ + QJSEngine *engine = s_jsThread->engine; + QJSValue audioObj = engine->newObject(); + AudioChannel *channel = NULL; + AudioChannelConfig config = AudioChannelConfig::defaults(); + Doc *currentDoc = doc(); + AudioProfile *profile = NULL; + if (currentDoc != NULL) + { + profile = currentDoc->audioProfile(currentDoc->activeAudioProfileId()); + if (profile == NULL) + profile = currentDoc->defaultAudioProfile(); + } + if (profile != NULL) + { + channel = profile->channel(); + config = (channel != NULL) ? channel->config() : profile->channelConfig(); + } + + AudioSnapshot snap; + if (channel != NULL) + snap = channel->snapshot(); + + const int onsetMethodIndex = std::clamp(config.aubio.onsetMethodIndex, + 0, AUBIO_ONSET_METHODS - 1); + + const bool tempoActive = snap.music.bpm > 0.0; + const double bpm = tempoActive ? snap.music.bpm : 120.0; + const double beatPhase = snap.music.beatPhase; + const double dt = (double(MasterTimer::tick()) / 1000.0) * (bpm / 60.0); + + audioObj.setProperty(QStringLiteral("beat"), QJSValue(snap.beatPower)); + audioObj.setProperty(QStringLiteral("bass"), QJSValue(snap.bassPower)); + audioObj.setProperty(QStringLiteral("low"), QJSValue(snap.lows)); + audioObj.setProperty(QStringLiteral("mid"), QJSValue(snap.mids)); + audioObj.setProperty(QStringLiteral("high"), QJSValue(snap.highs)); + audioObj.setProperty(QStringLiteral("onset"), QJSValue(onsetFiredAt(snap, onsetMethodIndex))); + audioObj.setProperty(QStringLiteral("onsetIntensity"), QJSValue(snap.onsets.thresholdedDescriptors[onsetMethodIndex])); + audioObj.setProperty(QStringLiteral("beatFired"), QJSValue(snap.beatTrigger.firedThisFrame)); + audioObj.setProperty(QStringLiteral("downbeat"), QJSValue(snap.downbeatFired)); + audioObj.setProperty(QStringLiteral("bpm"), QJSValue(bpm)); + audioObj.setProperty(QStringLiteral("phase"), QJSValue(beatPhase)); + audioObj.setProperty(QStringLiteral("barPhase"), QJSValue(snap.music.barPhase / double(kBeatsPerBar))); + audioObj.setProperty(QStringLiteral("dt"), QJSValue(tempoActive ? dt : 0.0)); + audioObj.setProperty(QStringLiteral("cosPulse"), QJSValue(tempoActive + ? std::max(0.0, std::cos(beatPhase * kPi)) : 0.0)); + audioObj.setProperty(QStringLiteral("version"), QJSValue(kAudioApiVersion)); + + return audioObj; +} diff --git a/engine/src/huescript.h b/engine/src/huescript.h new file mode 100644 index 0000000000..987914d772 --- /dev/null +++ b/engine/src/huescript.h @@ -0,0 +1,137 @@ +/* + Q Light Controller Plus + huescript.h + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUESCRIPT_H +#define HUESCRIPT_H + +#include +#include + +#include "rgbscriptv4.h" + +class AudioCapture; +class RGBMatrix; + +/** @addtogroup engine Engine + * @{ + */ + +/** + * A script algorithm usable by a HUEMatrix. + * + * On top of RGBScript it adds: + * - the HSV contract: rgbMap() may return a flat Float32Array of + * width*height*3 floats in [0,1] which is converted to packed RGB here. + * Scripts that still return the upstream nested packed-uint arrays keep + * working, so a HUEMatrix can run every stock RGB script unchanged. + * - live audio input, injected as a 5th rgbMap() argument. + * - the algo.colors / algo.hasUserColors / algo.displayWidth|Height + * injected properties. + */ +class HUEScript : public RGBScript +{ +public: + HUEScript(Doc *doc); + HUEScript(const HUEScript &s); + ~HUEScript(); + + /** @reimp */ + RGBAlgorithm *clone() const override; + + /** + * Select the contract this script is expected to honour. + * + * true -> rgbMap() returns a Float32Array of HSV triples and receives + * its colour arguments as {h,s,v} objects. + * false -> upstream behaviour (nested packed-uint arrays, packed uint + * colour arguments). + * + * Set by HUEScriptsCache from the directory the script was found in. + */ + void setHsvContract(bool hsv); + bool hsvContract() const; + + /** @reimp */ + bool evaluate() override; + + /** @reimp */ + void rgbMap(const QSize &size, uint rgb, int step, RGBMap &map) override; + + /** @reimp */ + void rgbMapSetColors(const QVector &colors) override; + + /** @reimp */ + bool usesAudio() const override; + + /** @reimp */ + QStringList audioInputCategories() const override; + + /** @reimp */ + void setDisplaySize(const QSize &size) override; + + /** @reimp */ + void postRun() override; + + /** + * Schedule a callable on the shared JS thread. Used to dispatch async + * rgbMap pre-computations without blocking the MasterTimer thread, and + * to defer deletion of script algorithms until in-flight tasks drained. + * + * Returns false when the JS thread is not running, in which case the + * caller must fall back to a synchronous path. Safe from any thread. + */ + static bool scheduleOnJSThread(std::function fn); + +protected: + /** Evaluate the shared HSV helper scripts into the engine global scope. */ + static void loadHsvShims(); + + /** Read algo.usesAudio / algo.audioInputs after a successful evaluate(). */ + void parseAudioMetadata(); + + /** Connect to AudioCapture and register for spectrum data */ + void setupAudioCapture(); + + /** Disconnect from AudioCapture and release resources */ + void teardownAudioCapture(); + + /** Build the JS object passed as the 5th rgbMap() argument */ + QJSValue buildAudioDataObject(); + + /** + * Inject the matrix colour palette as algo.colors (array of {h,s,v}) and + * algo.hasUserColors. + * + * @param rgb fallback colour (packed 0xRRGGBB) used when the matrix has + * no valid colour stops. + */ + void injectColors(uint rgb, RGBMatrix *matrix); + +protected: + bool m_hsvContract; //! Whether rgbMap() speaks HSV + bool m_usesAudio; //! algo.usesAudio was declared + QStringList m_audioInputCategories; //! Top-level audio.X categories used + AudioCapture *m_audioInput; + bool m_audioRegistered; + bool m_hsvContractValidated; //! Skip type checks after first frame +}; + +/** @} */ + +#endif // HUESCRIPT_H diff --git a/engine/src/huescriptscache.cpp b/engine/src/huescriptscache.cpp new file mode 100644 index 0000000000..b219008edb --- /dev/null +++ b/engine/src/huescriptscache.cpp @@ -0,0 +1,121 @@ +/* + Q Light Controller Plus + huescriptscache.cpp + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include + +#include "huescriptscache.h" +#include "huescript.h" +#include "qlcconfig.h" +#include "qlcfile.h" + +HUEScriptsCache::HUEScriptsCache(Doc *doc) + : m_doc(doc) +{ +} + +QStringList HUEScriptsCache::names() const +{ + return m_scriptsMap.keys(); +} + +QStringList HUEScriptsCache::hsvNames() const +{ + return m_hsvNames; +} + +HUEScript *HUEScriptsCache::script(QString name) const +{ + QString filename = m_scriptsMap.value(name); + if (filename.isEmpty()) + return NULL; + + HUEScript *script = new HUEScript(m_doc); + script->setHsvContract(m_hsvNames.contains(name)); + script->load(filename); + return script; +} + +QStringList HUEScriptsCache::s_hsvDirectories; + +QStringList HUEScriptsCache::hsvScriptDirectories() +{ + QStringList dirs = s_hsvDirectories; + if (dirs.contains(systemScriptsDirectory().absolutePath()) == false) + dirs << systemScriptsDirectory().absolutePath(); + return dirs; +} + +bool HUEScriptsCache::load(const QDir &dir, bool hsvContract) +{ + qDebug() << "Loading HUE scripts in" << dir.path() << "..."; + + if (dir.exists() == false || dir.isReadable() == false) + return false; + + if (hsvContract && s_hsvDirectories.contains(dir.absolutePath()) == false) + s_hsvDirectories << dir.absolutePath(); + + foreach (QString file, dir.entryList()) + { + if (!file.endsWith(".js", Qt::CaseInsensitive)) + continue; + + QFile absFile(dir.absoluteFilePath(file)); + QString absFilename = absFile.fileName(); + + if (!absFile.open(QIODevice::ReadOnly | QIODevice::Text)) + continue; + + QTextStream in(&absFile); + QString line = in.readLine(); + while (!line.isNull()) + { + QStringList tokens = line.split("="); + if (tokens.length() == 2 && tokens[0].simplified() == "algo.name") + { + QString algoName = tokens[1].simplified().remove('"'); + algoName.remove(';'); + if (m_scriptsMap.value(algoName).isEmpty()) + { + m_scriptsMap.insert(algoName, absFilename); + if (hsvContract) + m_hsvNames.append(algoName); + } + break; + } + line = in.readLine(); + } + absFile.close(); + } + return true; +} + +QDir HUEScriptsCache::systemScriptsDirectory() +{ + return QLCFile::systemDirectory(QString(HUESCRIPTDIR), QString(".js")); +} + +QDir HUEScriptsCache::userScriptsDirectory() +{ + return QLCFile::userDirectory(QString(USERHUESCRIPTDIR), QString(HUESCRIPTDIR), + QStringList() << QString("*%1").arg(".js")); +} diff --git a/engine/src/huescriptscache.h b/engine/src/huescriptscache.h new file mode 100644 index 0000000000..67a462f953 --- /dev/null +++ b/engine/src/huescriptscache.h @@ -0,0 +1,85 @@ +/* + Q Light Controller Plus + huescriptscache.h + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUESCRIPTSCACHE_H +#define HUESCRIPTSCACHE_H + +#include +#include + +class HUEScript; +class QDir; +class Doc; + +/** @addtogroup engine Engine + * @{ + */ + +/** + * Cache of the HSV-contract scripts available to a HUEMatrix. + * + * A HUEMatrix can run every upstream RGB script *and* the HSV scripts shipped + * in the huescripts directory, so this cache indexes both. A plain RGBMatrix + * only ever sees RGBScriptsCache, which never indexes the huescripts + * directory. + */ +class HUEScriptsCache final +{ +public: + explicit HUEScriptsCache(Doc *doc); + + /** Return the names of every cached script, HSV and RGB alike. */ + QStringList names() const; + + /** Return the names of the HSV-contract scripts only. */ + QStringList hsvNames() const; + + /** Get a script instance by name, or NULL if $name is unknown. */ + HUEScript *script(QString name) const; + + /** + * Load scripts from $dir. When $hsvContract is true the scripts are + * expected to return a flat Float32Array of HSV triples; otherwise the + * upstream nested packed-uint contract is assumed. + * + * Returns true if $dir was accessible, even when it holds no script. + */ + bool load(const QDir &dir, bool hsvContract); + + /** Default system directory holding the installed HSV scripts. */ + static QDir systemScriptsDirectory(); + + /** User directory holding custom HSV scripts. */ + static QDir userScriptsDirectory(); + + /** Directories the HSV-contract scripts were actually loaded from. The + * JS shims (hsvutil.js) live alongside them. */ + static QStringList hsvScriptDirectories(); + +private: + Doc *m_doc; + QMap m_scriptsMap; //! name -> absolute file name + QStringList m_hsvNames; //! names loaded with the HSV contract + + static QStringList s_hsvDirectories; //! dirs that yielded HSV scripts +}; + +/** @} */ + +#endif // HUESCRIPTSCACHE_H diff --git a/engine/src/jsthread_p.h b/engine/src/jsthread_p.h new file mode 100644 index 0000000000..e32b5ab9a8 --- /dev/null +++ b/engine/src/jsthread_p.h @@ -0,0 +1,53 @@ +/* + Q Light Controller Plus + jsthread_p.h + + Copyright (c) QLC+ contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef JSTHREAD_P_H +#define JSTHREAD_P_H + +#include +#include + +#if defined(QT_QML_LIB) +#include + +/** + * Token-identical copy of the JSThread definition in rgbscriptv4.cpp. + * + * rgbscriptv4.cpp is kept byte-identical to upstream, so the class cannot be + * relocated into a shared header. Repeating the definition verbatim in another + * translation unit is well-formed under the one-definition rule as long as the + * two definitions consist of the same token sequence; keep them in sync. + */ +class JSThread final : public QThread +{ +public: + QJSEngine *engine; + QSemaphore ready; + void run() override + { + engine = new QJSEngine(); + ready.release(1); + exec(); + delete engine; + } +}; + +#endif // QT_QML_LIB + +#endif // JSTHREAD_P_H diff --git a/engine/src/qlcconfig.h.in b/engine/src/qlcconfig.h.in index 6277d0785b..f876b7a576 100644 --- a/engine/src/qlcconfig.h.in +++ b/engine/src/qlcconfig.h.in @@ -19,6 +19,8 @@ #define TRANSLATIONDIR "${INSTALLROOT}/${TRANSLATIONDIR}" #define RGBSCRIPTDIR "${INSTALLROOT}/${RGBSCRIPTDIR}" #define USERRGBSCRIPTDIR "${USERRGBSCRIPTDIR}" +#define HUESCRIPTDIR "${INSTALLROOT}/${HUESCRIPTDIR}" +#define USERHUESCRIPTDIR "${USERHUESCRIPTDIR}" #define GOBODIR "${INSTALLROOT}/${GOBODIR}" #define WEBFILESDIR "${INSTALLROOT}/${WEBFILESDIR}" diff --git a/engine/src/qlcconfig.h.noroot.in b/engine/src/qlcconfig.h.noroot.in index 8096bd2136..db96eb094b 100644 --- a/engine/src/qlcconfig.h.noroot.in +++ b/engine/src/qlcconfig.h.noroot.in @@ -19,6 +19,8 @@ #define TRANSLATIONDIR "${TRANSLATIONDIR}" #define RGBSCRIPTDIR "${RGBSCRIPTDIR}" #define USERRGBSCRIPTDIR "${USERRGBSCRIPTDIR}" +#define HUESCRIPTDIR "${HUESCRIPTDIR}" +#define USERHUESCRIPTDIR "${USERHUESCRIPTDIR}" #define GOBODIR "${GOBODIR}" #define WEBFILESDIR "${WEBFILESDIR}" diff --git a/engine/src/rgbalgorithm.h b/engine/src/rgbalgorithm.h index 800ef4ad58..aba5e9148d 100644 --- a/engine/src/rgbalgorithm.h +++ b/engine/src/rgbalgorithm.h @@ -105,6 +105,23 @@ class RGBAlgorithm */ virtual int acceptColors() const = 0; + /************************************************************************ + * Additive fork-only hooks (see HUEMatrix / HUEScript) + * + * Defaults keep every upstream algorithm behaving exactly as before. + ************************************************************************/ +public: + /** Whether the algorithm consumes live audio input. + * The built-in RGBAudio spectrum algorithm is audio-driven by definition, + * so the default keys off the algorithm type rather than being hard false. */ + virtual bool usesAudio() const { return type() == Audio; } + + /** Audio input categories the algorithm requests */ + virtual QStringList audioInputCategories() const { return QStringList(); } + + /** Inform the algorithm of the display size it renders into */ + virtual void setDisplaySize(const QSize &size) { Q_UNUSED(size) } + /************************************************************************ * RGB Colors ************************************************************************/ diff --git a/engine/src/rgbmatrix.h b/engine/src/rgbmatrix.h index 6316804105..063345018d 100644 --- a/engine/src/rgbmatrix.h +++ b/engine/src/rgbmatrix.h @@ -92,11 +92,16 @@ class RGBMatrixStep final int m_crDelta, m_cgDelta, m_cbDelta; }; -class RGBMatrix final : public Function +class RGBMatrix : public Function { Q_OBJECT Q_DISABLE_COPY(RGBMatrix) + /* Fork-only subclass; needs the same internals as RGBMatrix itself. + * A friend declaration is used instead of widening access specifiers, + * because the upstream unit tests rely on a `#define private public` hack. */ + friend class HUEMatrix; + /********************************************************************* * Initialization *********************************************************************/ @@ -140,7 +145,7 @@ class RGBMatrix final : public Function quint32 totalDuration() override; /** Set the matrix to control or not the dimmer channel */ - void setDimmerControl(bool dimmerControl); + virtual void setDimmerControl(bool dimmerControl); /** Get the matrix ability to control the dimmer channel */ bool dimmerControl() const; @@ -165,7 +170,7 @@ class RGBMatrix final : public Function public: /** Get/Set the Fixture Group associated to this RGBMatrix */ quint32 fixtureGroup() const; - void setFixtureGroup(quint32 id); + virtual void setFixtureGroup(quint32 id); /** @reimp */ QList components() const override; @@ -179,7 +184,7 @@ class RGBMatrix final : public Function ************************************************************************/ public: /** Set the current RGB Algorithm. RGBMatrix takes ownership of the pointer. */ - void setAlgorithm(RGBAlgorithm* algo); + virtual void setAlgorithm(RGBAlgorithm* algo); /** Get the current RGB Algorithm. */ RGBAlgorithm* algorithm() const; @@ -195,10 +200,10 @@ class RGBMatrix final : public Function int stepsCount() const; /** Get the preview of the current algorithm at the given step */ - void previewMap(int step, RGBMatrixStep *handler); + virtual void previewMap(int step, RGBMatrixStep *handler); private: - int algorithmStepsCount(); + virtual int algorithmStepsCount(); private: bool m_requestEngineCreation; @@ -292,7 +297,7 @@ class RGBMatrix final : public Function private: /** Check what should be done when elapsed() >= duration() */ - void roundCheck(); + virtual void roundCheck(); /** Check if the engine needs to be re-created */ void checkEngineCreation(); @@ -352,12 +357,16 @@ class RGBMatrix final : public Function ControlModeAmber, ControlModeUV, ControlModeDimmer, - ControlModeShutter + ControlModeShutter, + /* Additive fork-only modes (see HUEMatrix). Never set on a plain RGBMatrix. + * Explicit values guarantee no upstream enumerator is renumbered. */ + ControlModeRgbw = 6, + ControlModeRgbwBrighter = 7 }; /** Get/Set the control mode associated to this RGBMatrix */ RGBMatrix::ControlMode controlMode() const; - void setControlMode(RGBMatrix::ControlMode mode); + virtual void setControlMode(RGBMatrix::ControlMode mode); /** Return a control mode from a string */ static RGBMatrix::ControlMode stringToControlMode(QString mode); diff --git a/engine/src/rgbscriptscache.cpp b/engine/src/rgbscriptscache.cpp index 1273abdfc3..761622eee1 100644 --- a/engine/src/rgbscriptscache.cpp +++ b/engine/src/rgbscriptscache.cpp @@ -45,6 +45,9 @@ RGBScript* RGBScriptsCache::script(QString name) const QString filename = m_scriptsMap.value(name); if (filename.isEmpty()) { + qWarning() << "RGB script" << name << "is not available to RGBMatrix." + << "HSV audio scripts live in the HUE script directory and" + << "are only offered to HUEMatrix."; return mScript; } else diff --git a/engine/src/rgbscriptv4.h b/engine/src/rgbscriptv4.h index d6da2d9b3f..a9c2693cab 100644 --- a/engine/src/rgbscriptv4.h +++ b/engine/src/rgbscriptv4.h @@ -37,8 +37,11 @@ class JSThread; #define KXMLQLCRGBScript QStringLiteral("Script") -class RGBScript final : public RGBAlgorithm +class RGBScript : public RGBAlgorithm { + /* See the note on RGBMatrix::friend class HUEMatrix. */ + friend class HUEScript; + /************************************************************************ * Initialization ************************************************************************/ @@ -66,7 +69,7 @@ class RGBScript final : public RGBAlgorithm QString fileName() const; /** Evaluate the script's contents and see if it checks out */ - bool evaluate(); + virtual bool evaluate(); private: static void initEngine(); diff --git a/engine/src/showfunction.cpp b/engine/src/showfunction.cpp index b74a9638a9..282f36e706 100644 --- a/engine/src/showfunction.cpp +++ b/engine/src/showfunction.cpp @@ -126,6 +126,7 @@ QColor ShowFunction::defaultColor(Function::Type type) case Function::ChaserType: return QColor(85, 107, 128); case Function::AudioType: return QColor(96, 128, 83); case Function::RGBMatrixType: return QColor(101, 155, 155); + case Function::HUEMatrixType: return QColor(101, 155, 155); case Function::EFXType: return QColor(128, 60, 60); case Function::VideoType: return QColor(147, 140, 20); default: return QColor(100, 100, 100); diff --git a/engine/test/CMakeLists.txt b/engine/test/CMakeLists.txt index 2ece04d6f7..7cd19c87ae 100644 --- a/engine/test/CMakeLists.txt +++ b/engine/test/CMakeLists.txt @@ -56,6 +56,7 @@ add_subdirectory(qlcmodifierscache) add_subdirectory(qlcpalette) add_subdirectory(qlcphysical) add_subdirectory(qlcpoint) +add_subdirectory(huematrix) add_subdirectory(rgbalgorithm) add_subdirectory(rgbmatrix) add_subdirectory(rgbplain) diff --git a/engine/test/function/function_test.cpp b/engine/test/function/function_test.cpp index 84b0fe743f..861ca72bbd 100644 --- a/engine/test/function/function_test.cpp +++ b/engine/test/function/function_test.cpp @@ -287,6 +287,7 @@ void Function_Test::typeToString() QVERIFY(Function::typeToString(Function::EFXType) == "EFX"); QVERIFY(Function::typeToString(Function::CollectionType) == "Collection"); QVERIFY(Function::typeToString(Function::RGBMatrixType) == "RGBMatrix"); + QVERIFY(Function::typeToString(Function::HUEMatrixType) == "HUEMatrix"); QVERIFY(Function::typeToString(Function::ScriptType) == "Script"); QVERIFY(Function::typeToString(Function::SequenceType) == "Sequence"); QVERIFY(Function::typeToString(Function::ShowType) == "Show"); @@ -305,6 +306,8 @@ void Function_Test::stringToType() QVERIFY(Function::stringToType("EFX") == Function::EFXType); QVERIFY(Function::stringToType("Collection") == Function::CollectionType); QVERIFY(Function::stringToType("RGBMatrix") == Function::RGBMatrixType); + QVERIFY(Function::stringToType("HUEMatrix") == Function::HUEMatrixType); + QVERIFY(Function::stringToType("HUEMatrixx") == Function::Undefined); QVERIFY(Function::stringToType("Script") == Function::ScriptType); QVERIFY(Function::stringToType("Sequence") == Function::SequenceType); QVERIFY(Function::stringToType("Show") == Function::ShowType); diff --git a/engine/test/huematrix/CMakeLists.txt b/engine/test/huematrix/CMakeLists.txt new file mode 100644 index 0000000000..cde25a1551 --- /dev/null +++ b/engine/test/huematrix/CMakeLists.txt @@ -0,0 +1,38 @@ +add_executable(huematrix_test WIN32 + ../common/resource_paths.h + huematrix_test.cpp huematrix_test.h +) +target_include_directories(huematrix_test PRIVATE + ../../../plugins/interfaces + ../../src + ../mastertimer +) + +# AC17 asserts on the shipped icon artifacts, which are not copied into the +# test's runtime resource directory. +target_compile_definitions(huematrix_test PRIVATE + QLCPLUS_SOURCE_DIR="${CMAKE_SOURCE_DIR}" +) + +target_link_libraries(huematrix_test PRIVATE + Qt${QT_MAJOR_VERSION}::Core + Qt${QT_MAJOR_VERSION}::Gui + Qt${QT_MAJOR_VERSION}::Test + qlcplusengine +) + +if(qmlui OR (QT_VERSION_MAJOR GREATER 5)) + target_link_libraries(huematrix_test PRIVATE + Qt${QT_MAJOR_VERSION}::Qml + ) +endif() + +if(NOT (qmlui OR (QT_VERSION_MAJOR GREATER 5))) + target_link_libraries(huematrix_test PRIVATE + Qt${QT_MAJOR_VERSION}::Script + ) +endif() + +# Consider using qt_generate_deploy_app_script() for app deployment if +# the project can use Qt 6.3. In that case rerun qmake2cmake with +# --min-qt-version=6.3. diff --git a/engine/test/huematrix/huematrix_test.cpp b/engine/test/huematrix/huematrix_test.cpp new file mode 100644 index 0000000000..0dad1e10dc --- /dev/null +++ b/engine/test/huematrix/huematrix_test.cpp @@ -0,0 +1,1316 @@ +/* + Q Light Controller Plus - Unit test + huematrix_test.cpp + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define private public +#define protected public +#include "huematrix_test.h" +#include "huescriptscache.h" +#include "huescript.h" +#include "huematrix.h" +#include "rgbalgorithm.h" +#include "rgbaudio.h" +#include "rgbimage.h" +#include "rgbplain.h" +#include "rgbtext.h" +#include "rgbmatrix.h" +#include "rgbscriptscache.h" +#include "fixturegroup.h" +#include "inputoutputmap.h" +#include "mastertimer.h" +#include "universe.h" +#include "function.h" +#undef private +#undef protected + +#include "doc.h" + +#include "../common/resource_paths.h" + +#define INTERNAL_HUESCRIPTDIR "../../../resources/huescripts/" + +void HUEMatrix_Test::initTestCase() +{ + m_doc = new Doc(this); + QVERIFY(m_doc->rgbScriptsCache()->load(QDir(INTERNAL_SCRIPTDIR))); + QVERIFY(m_doc->hueScriptsCache()->load(QDir(INTERNAL_HUESCRIPTDIR), true)); + QVERIFY(m_doc->hueScriptsCache()->load(QDir(INTERNAL_SCRIPTDIR), false)); +} + +void HUEMatrix_Test::cleanupTestCase() +{ + delete m_doc; + m_doc = NULL; +} + +/**************************************************************************** + * AC1 - HUEMatrix is a first class Function type + ****************************************************************************/ + +void HUEMatrix_Test::functionType() +{ + HUEMatrix mtx(m_doc); + QCOMPARE(mtx.type(), Function::HUEMatrixType); + QCOMPARE(Function::typeToString(mtx.type()), QString("HUEMatrix")); + QCOMPARE(Function::stringToType("HUEMatrix"), Function::HUEMatrixType); + + // A HUEMatrix is-a RGBMatrix, but a plain RGBMatrix is never a HUEMatrix + RGBMatrix plain(m_doc); + QCOMPARE(plain.type(), Function::RGBMatrixType); + QVERIFY(qobject_cast(&plain) == NULL); + QVERIFY(qobject_cast(&mtx) != NULL); +} + +/**************************************************************************** + * AC5 - script partitioning + ****************************************************************************/ + +void HUEMatrix_Test::audioScriptsAreNotOfferedToRGBMatrix() +{ + // THE IMPORTANT HALF: a plain RGBMatrix must not see any relocated script. + QStringList rgbScripts = m_doc->rgbScriptsCache()->names(); + QVERIFY(rgbScripts.isEmpty() == false); + + QStringList leaked; + foreach (QString name, m_doc->hueScriptsCache()->hsvNames()) + { + if (rgbScripts.contains(name)) + leaked << name; + } + QVERIFY2(leaked.isEmpty(), + qPrintable(QString("HSV scripts leaked into the RGBMatrix script list: %1") + .arg(leaked.join(", ")))); + + // No file in resources/rgbscripts is named audio*.js any more + QStringList strays = QDir(INTERNAL_SCRIPTDIR).entryList(QStringList() << "audio*.js"); + QVERIFY2(strays.isEmpty(), + qPrintable(QString("audio scripts left in rgbscripts: %1").arg(strays.join(", ")))); + + // And resources/huescripts holds exactly those files + QStringList moved = QDir(INTERNAL_HUESCRIPTDIR).entryList(QStringList() << "audio*.js"); + QCOMPARE(moved.count(), 41); +} + +void HUEMatrix_Test::hueMatrixOffersAllAudioScripts() +{ + QStringList hueNames = HUEMatrix::availableAlgorithms(m_doc); + QStringList rgbNames = RGBAlgorithm::algorithms(m_doc); + + // Strict superset of the RGBMatrix list + foreach (QString name, rgbNames) + QVERIFY2(hueNames.contains(name), qPrintable(name)); + QVERIFY(hueNames.count() > rgbNames.count()); + + // All 41 relocated audio scripts are present and instantiable + QStringList hsv = m_doc->hueScriptsCache()->hsvNames(); + QCOMPARE(hsv.count(), 41); + foreach (QString name, hsv) + { + QVERIFY2(hueNames.contains(name), qPrintable(name)); + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(algo != NULL, qPrintable(name)); + QCOMPARE(algo->name(), name); + delete algo; + } +} + +/**************************************************************************** + * AC4 - the HSV contract really produces pixels + ****************************************************************************/ + +void HUEMatrix_Test::hsvScriptProducesFiniteNonUniformMap() +{ + QStringList hsv = m_doc->hueScriptsCache()->hsvNames(); + QVERIFY(hsv.isEmpty() == false); + + QStringList nonUniform; + foreach (QString name, hsv) + { + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(algo != NULL, qPrintable(name)); + QCOMPARE(algo->type(), RGBAlgorithm::Script); + algo->setDisplaySize(QSize(5, 5)); + + // Every HSV script must decode into a well formed 5x5 packed-RGB map. + // A Float32Array that was not understood would leave the map empty. + uint first = 0; + for (int step = 0; step < 3; step++) + { + RGBMap map; + algo->rgbMap(QSize(5, 5), 0x00FF0000, step, map); + QCOMPARE(map.size(), 5); + for (int y = 0; y < 5; y++) + { + QCOMPARE(map[y].size(), 5); + for (int x = 0; x < 5; x++) + { + uint px = map[y][x]; + // Never a NaN-derived or out of range channel + QVERIFY2((px & 0xFF000000) == 0 || qAlpha(px) == 255, qPrintable(name)); + QVERIFY(qRed(px) >= 0 && qRed(px) <= 255); + QVERIFY(qGreen(px) >= 0 && qGreen(px) <= 255); + QVERIFY(qBlue(px) >= 0 && qBlue(px) <= 255); + if (y == 0 && x == 0 && step == 0) + first = px; + else if (px != first && nonUniform.contains(name) == false) + nonUniform << name; + } + } + } + delete algo; + } + + // At least one HSV script must paint an actually varying picture, which + // proves the HSV -> packed RGB conversion carries real values through. + QVERIFY2(nonUniform.isEmpty() == false, + "no HSV script produced a non-uniform map"); + qDebug() << "non-uniform HSV scripts:" << nonUniform.count() << "of" << hsv.count(); +} + +/**************************************************************************** + * AC6 - upstream scripts keep working under the HUE script wrapper + ****************************************************************************/ + +void HUEMatrix_Test::upstreamScriptStillWorksOnHueScript() +{ + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, "Stripes"); + QVERIFY(algo != NULL); + QCOMPARE(algo->name(), QString("Stripes")); + QVERIFY(dynamic_cast(algo) != NULL); + QVERIFY(algo->usesAudio() == false); + + RGBScript *script = static_cast(algo); + script->setProperty("orientation", "Vertical"); + QCOMPARE(script->property("orientation"), QString("Vertical")); + + // Same assertion the restored rgbscript_test makes: for a 5x5 map at + // step N, row N is the step colour and every other row is the second one. + QVector rawRgbColors = { QColor(Qt::red).rgb(), uint(0) }; + + for (int step = 0; step < 5; step++) + { + RGBMap map; + script->rgbMap(QSize(5, 5), rawRgbColors[0], step, map); + for (int y = 0; y < 5; y++) + { + for (int x = 0; x < 5; x++) + { + if (y == step) + QCOMPARE(map[y][x], rawRgbColors[0]); + else + QCOMPARE(map[y][x], rawRgbColors[1]); + } + } + } + delete algo; +} + +/**************************************************************************** + * AC7 - every fork property is reachable on HUEMatrix + ****************************************************************************/ + +void HUEMatrix_Test::forkPropertyRoundTripsInMemory_data() +{ + QTest::addColumn("property"); + QTest::addColumn("value"); + + QTest::newRow("rotation 90") << "rotation" << 1; + QTest::newRow("rotation 270") << "rotation" << 3; + QTest::newRow("mirror horizontal") << "mirror" << 1; + QTest::newRow("mirror both") << "mirror" << 3; + QTest::newRow("mirrorBlend max") << "mirrorBlend" << int(HUEMatrix::MirrorMax); + QTest::newRow("mirrorBlend average") << "mirrorBlend" << int(HUEMatrix::MirrorAverage); + QTest::newRow("beatEffect strobe") << "beatEffect" << int(HUEMatrix::BeatEffectBlackout); + QTest::newRow("beatSelection random") << "beatSelection" << int(HUEMatrix::BeatSelRandom); + QTest::newRow("beatOrientation cols") << "beatOrientation" << int(HUEMatrix::BeatOrientColumns); + QTest::newRow("controlMode rgbw") << "controlMode" << int(RGBMatrix::ControlModeRgbw); + QTest::newRow("controlMode rgbwBright") << "controlMode" << int(RGBMatrix::ControlModeRgbwBrighter); +} + +void HUEMatrix_Test::forkPropertyRoundTripsInMemory() +{ + QFETCH(QString, property); + QFETCH(int, value); + + HUEMatrix mtx(m_doc); + + if (property == "rotation") + { + QCOMPARE(mtx.rotation(), 0); + mtx.setRotation(value); + QCOMPARE(mtx.rotation(), value); + } + else if (property == "mirror") + { + QCOMPARE(mtx.mirror(), 0); + mtx.setMirror(value); + QCOMPARE(mtx.mirror(), value); + } + else if (property == "mirrorBlend") + { + QCOMPARE(mtx.mirrorBlend(), HUEMatrix::MirrorFlip); + mtx.setMirrorBlend(HUEMatrix::MirrorBlend(value)); + QCOMPARE(int(mtx.mirrorBlend()), value); + } + else if (property == "beatEffect") + { + QCOMPARE(mtx.beatEffect(), HUEMatrix::BeatEffectOff); + mtx.setBeatEffect(HUEMatrix::BeatEffect(value)); + QCOMPARE(int(mtx.beatEffect()), value); + } + else if (property == "beatSelection") + { + QCOMPARE(mtx.beatSelection(), HUEMatrix::BeatSelAllOnDownbeat); + mtx.setBeatSelection(HUEMatrix::BeatSelection(value)); + QCOMPARE(int(mtx.beatSelection()), value); + } + else if (property == "beatOrientation") + { + QCOMPARE(mtx.beatOrientation(), HUEMatrix::BeatOrientRows); + mtx.setBeatOrientation(HUEMatrix::BeatOrientation(value)); + QCOMPARE(int(mtx.beatOrientation()), value); + } + else if (property == "controlMode") + { + QCOMPARE(mtx.controlMode(), RGBMatrix::ControlModeRgb); + mtx.setControlMode(RGBMatrix::ControlMode(value)); + QCOMPARE(int(mtx.controlMode()), value); + } + else + { + QFAIL("unhandled property"); + } + + // Brightness is a qreal and gets its own clamp check here + QCOMPARE(mtx.brightness(), qreal(1.0)); + mtx.setBrightness(0.25); + QCOMPARE(mtx.brightness(), qreal(0.25)); +} + +/**************************************************************************** + * AC8 - XML round trip + ****************************************************************************/ + +void HUEMatrix_Test::forkPropertiesSurviveXmlRoundTrip() +{ + HUEMatrix *mtx = new HUEMatrix(m_doc); + mtx->setName("Fork Round Trip"); + mtx->setControlMode(RGBMatrix::ControlModeRgbwBrighter); + mtx->setRotation(3); + mtx->setMirror(3); + mtx->setMirrorBlend(HUEMatrix::MirrorAverage); + mtx->setBrightness(0.375); + mtx->setBeatEffect(HUEMatrix::BeatEffectBlackout); + mtx->setBeatSelection(HUEMatrix::BeatSelRandom); + mtx->setBeatOrientation(HUEMatrix::BeatOrientColumns); + mtx->setAlgorithm(HUEMatrix::createAlgorithm(m_doc, "Audio Fire")); + QVERIFY(mtx->algorithm() != NULL); + m_doc->addFunction(mtx); + + QBuffer buffer; + buffer.open(QIODevice::WriteOnly | QIODevice::Text); + QXmlStreamWriter xmlWriter(&buffer); + QVERIFY(mtx->saveXML(&xmlWriter) == true); + xmlWriter.setDevice(NULL); + buffer.close(); + + buffer.open(QIODevice::ReadOnly | QIODevice::Text); + QXmlStreamReader xmlReader(&buffer); + xmlReader.readNextStartElement(); + QCOMPARE(xmlReader.name().toString(), QString("Function")); + QCOMPARE(xmlReader.attributes().value("Type").toString(), QString("HUEMatrix")); + + HUEMatrix reloaded(m_doc); + QVERIFY(reloaded.loadXML(xmlReader) == true); + + QCOMPARE(reloaded.controlMode(), RGBMatrix::ControlModeRgbwBrighter); + QCOMPARE(reloaded.rotation(), 3); + QCOMPARE(reloaded.mirror(), 3); + QCOMPARE(reloaded.mirrorBlend(), HUEMatrix::MirrorAverage); + QCOMPARE(reloaded.brightness(), qreal(0.375)); + QCOMPARE(reloaded.beatEffect(), HUEMatrix::BeatEffectBlackout); + QCOMPARE(reloaded.beatSelection(), HUEMatrix::BeatSelRandom); + QCOMPARE(reloaded.beatOrientation(), HUEMatrix::BeatOrientColumns); + QVERIFY(reloaded.algorithm() != NULL); + QCOMPARE(reloaded.algorithm()->name(), QString("Audio Fire")); +} + +void HUEMatrix_Test::unknownXmlValuesFallBackToDefaults() +{ + // ERROR CASE: garbage enum text must not corrupt the matrix + QString xml = "" + "" + "Forward" + "Loop" + "Nonsense" + "Nonsense" + "Nonsense" + "Nonsense" + "Nonsense" + ""; + + QXmlStreamReader reader(xml); + reader.readNextStartElement(); + + HUEMatrix mtx(m_doc); + QVERIFY(mtx.loadXML(reader) == true); + QCOMPARE(mtx.controlMode(), RGBMatrix::ControlModeRgb); + QCOMPARE(mtx.mirrorBlend(), HUEMatrix::MirrorFlip); + QCOMPARE(mtx.beatEffect(), HUEMatrix::BeatEffectOff); + QCOMPARE(mtx.beatSelection(), HUEMatrix::BeatSelAllOnDownbeat); + QCOMPARE(mtx.beatOrientation(), HUEMatrix::BeatOrientRows); + + // And a node that is not a Function at all is rejected + QXmlStreamReader bad(""); + bad.readNextStartElement(); + HUEMatrix other(m_doc); + QVERIFY(other.loadXML(bad) == false); +} + +/**************************************************************************** + * AC9 - unavailable algorithm + ****************************************************************************/ + +void HUEMatrix_Test::unavailableAlgorithmIsRejected() +{ + QVERIFY(HUEMatrix::createAlgorithm(m_doc, "No Such Algorithm At All") == NULL); + + QString xml = "" + "" + "Forward" + "Loop" + "No Such Algorithm At All" + ""; + + QXmlStreamReader reader(xml); + reader.readNextStartElement(); + + HUEMatrix mtx(m_doc); + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression("Script \"No Such Algorithm At All\" not available")); + QVERIFY(mtx.loadXML(reader) == true); + // The function still loads, it simply has no algorithm to run + QVERIFY(mtx.algorithm() == NULL); +} + +/**************************************************************************** + * AC11 - upstream e671868c6 attribute surface survived the restore + ****************************************************************************/ + +void HUEMatrix_Test::scriptPropertyAttrDoesNotCollide() +{ + // ScriptPropertyAttr is the first index AFTER every fixed attribute + QVERIFY(RGBMatrix::ScriptPropertyAttr > RGBMatrix::ColorLastAttr); + QCOMPARE(int(RGBMatrix::ScriptPropertyAttr), int(RGBMatrix::PatternAttr) + 1); + QCOMPARE(int(RGBMatrix::PatternAttr), int(RGBMatrix::ColorLastAttr) + 1); + QCOMPARE(int(RGBMatrix::Color1Attr), int(Function::Intensity) + 1); + + // The two additive control modes must not shadow any upstream value + QCOMPARE(int(RGBMatrix::ControlModeRgbw), 6); + QCOMPARE(int(RGBMatrix::ControlModeRgbwBrighter), 7); + QVERIFY(int(RGBMatrix::ControlModeShutter) < int(RGBMatrix::ControlModeRgbw)); + + // Upstream RGBMatrix registers script property attributes for a script + // Upstream RGBMatrix exposes script properties as Function attributes, + // starting exactly at ScriptPropertyAttr. + RGBMatrix mtx(m_doc); + mtx.setAlgorithm(NULL); + int fixed = mtx.attributes().count(); + QCOMPARE(fixed, int(RGBMatrix::ScriptPropertyAttr)); + QCOMPARE(mtx.scriptPropertyAttributes().count(), 0); + + mtx.setAlgorithm(RGBAlgorithm::algorithm(m_doc, "Stripes")); + QVERIFY(mtx.scriptPropertyAttributes().count() > 0); + QCOMPARE(mtx.attributes().count(), + fixed + mtx.scriptPropertyAttributes().count()); + QCOMPARE(mtx.attributes().at(RGBMatrix::ScriptPropertyAttr).m_name, + RGBMatrix::scriptPropertyAttributeName(mtx.scriptPropertyAttributes().first())); + + // Dropping the algorithm again removes exactly those attributes + mtx.setAlgorithm(NULL); + QCOMPARE(mtx.attributes().count(), fixed); +} + +/**************************************************************************** + * AC12 - HUEMatrix is compatible with that attribute surface + ****************************************************************************/ + +void HUEMatrix_Test::hueMatrixSupportsScriptPropertyAttributes() +{ + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(HUEMatrix::createAlgorithm(m_doc, "Stripes")); + QVERIFY(mtx.algorithm() != NULL); + + QList props = mtx.scriptPropertyAttributes(); + QVERIFY2(props.count() > 0, "HUEMatrix did not register script properties"); + + // Drive a real script property through the PUBLIC attribute API + int idx = RGBMatrix::ScriptPropertyAttr; + QCOMPARE(mtx.adjustAttribute(1.0, idx), idx); + QCOMPARE(mtx.getAttributeValue(idx), qreal(1.0)); + + RGBScript *script = static_cast(mtx.algorithm()); + QString name = props.first().m_name; + QVERIFY(script->property(name).isEmpty() == false); + + // Out of range attribute index is rejected, not crashed on + QCOMPARE(mtx.adjustAttribute(1.0, 9999), -1); +} + +void HUEMatrix_Test::hueMatrixSupportsForkAttributes() +{ + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(HUEMatrix::createAlgorithm(m_doc, "Audio Fire")); + QVERIFY(mtx.algorithm() != NULL); + + // A fork-only script property (Audio Fire lives in resources/huescripts) + QList props = mtx.scriptPropertyAttributes(); + QVERIFY2(props.count() > 0, "fork-only script exposed no properties"); + + bool sawIntensity = false; + foreach (RGBScriptProperty p, props) + { + if (p.m_name == "intensity") + sawIntensity = true; + } + QVERIFY2(sawIntensity, "Audio Fire 'intensity' property is not an attribute"); + + int idx = RGBMatrix::ScriptPropertyAttr; + QCOMPARE(mtx.adjustAttribute(0.5, idx), idx); + QCOMPARE(mtx.getAttributeValue(idx), qreal(0.5)); + + // The upstream fixed attributes still work on a HUEMatrix + QCOMPARE(mtx.adjustAttribute(0.5, Function::Intensity), int(Function::Intensity)); + QCOMPARE(mtx.getAttributeValue(Function::Intensity), qreal(0.5)); +} + +/**************************************************************************** + * Built-in algorithm edge case - no script involved + ****************************************************************************/ + +void HUEMatrix_Test::builtInAlgorithmStillRunsTransformPipeline() +{ + // Every built-in is instantiable on a HUEMatrix and is not a script + foreach (QString name, QStringList() << "Plain Color" << "Text" << "Image") + { + RGBAlgorithm *builtIn = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(builtIn != NULL, qPrintable(name)); + QVERIFY2(builtIn->type() != RGBAlgorithm::Script, qPrintable(name)); + delete builtIn; + } + + // And a HUEMatrix driving a built-in still runs the fork's transform and + // beat pipeline over that built-in's map. + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, "Plain Color"); + QVERIFY(algo != NULL); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(algo); + mtx.setRotation(1); + mtx.setMirror(1); + mtx.setBeatEffect(HUEMatrix::BeatEffectBlackout); + QCOMPARE(mtx.rotation(), 1); + QCOMPARE(mtx.beatEffect(), HUEMatrix::BeatEffectBlackout); + + RGBMap map; + algo->rgbMap(QSize(4, 3), 0x00FF8000, 0, map); + QCOMPARE(map.size(), 3); + QCOMPARE(map[0].size(), 4); + + // A quarter turn swaps the axes + HUEMatrix::applyTransforms(map, QSize(4, 3), QSize(3, 4), 1, 1, + HUEMatrix::MirrorFlip); + QCOMPARE(map.size(), 4); + QCOMPARE(map[0].size(), 3); + + // Blackout on the downbeat must clear the map without resizing it + mtx.applyBeatTransform(map, 0, 4); + QCOMPARE(map.size(), 4); + QCOMPARE(map[0].size(), 3); +} + +/**************************************************************************** + * AC13: every QJSValue touch must be marshalled onto the JS engine thread. + * + * HUEMatrix::write() runs on the MasterTimer thread, so rgbMap() is routinely + * called from a thread that does not own the engine. Without the guards this + * faulted inside QV4::PersistentValueStorage::allocate. + ****************************************************************************/ + +void HUEMatrix_Test::hsvScriptRgbMapIsSafeFromAnotherThread() +{ + QStringList hsv = m_doc->hueScriptsCache()->hsvNames(); + QVERIFY(hsv.isEmpty() == false); + + QString name = hsv.first(); + + // evaluate() happens here, on the main thread... + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(algo != NULL, qPrintable(name)); + + // ...while every render happens on a foreign thread, as it does live. + struct Renderer : public QThread + { + RGBAlgorithm *algo; + int filled = 0; + void run() override + { + algo->setDisplaySize(QSize(5, 5)); + for (int step = 0; step < 20; step++) + { + RGBMap map; + algo->rgbMap(QSize(5, 5), 0x0000FF00, step, map); + if (map.size() == 5 && map[0].size() == 5) + filled++; + } + } + } renderer; + + renderer.algo = algo; + renderer.start(); + QVERIFY2(renderer.wait(60000), "render thread did not finish - deadlock"); + QCOMPARE(renderer.filled, 20); + + delete algo; +} + +/**************************************************************************** + * AC16: RGBAudio must keep reporting that it is audio driven, otherwise + * HUEMatrix::write() stops recomputing its map every tick. + ****************************************************************************/ + +void HUEMatrix_Test::builtInAudioAlgorithmReportsUsesAudio() +{ + // AC19 resolved the "Audio Spectrum" shadowing, so the HUE factory now + // returns the genuine built-in for this name. + RGBAlgorithm *audio = HUEMatrix::createAlgorithm(m_doc, RGBAudio(m_doc).name()); + QVERIFY(audio != NULL); + QCOMPARE(audio->type(), RGBAlgorithm::Audio); + QCOMPARE(audio->usesAudio(), true); + delete audio; + + // The default must not leak onto algorithms that are not audio driven. + RGBAlgorithm *plain = HUEMatrix::createAlgorithm(m_doc, "Stripes"); + QVERIFY(plain != NULL); + QVERIFY(plain->type() != RGBAlgorithm::Audio); + QCOMPARE(plain->usesAudio(), false); + delete plain; +} + +/**************************************************************************** + * AC14: the HUE cache must be populated the way the app startup path does it, + * so that HUEMatrixEditor::algorithms() - which returns exactly this list - + * shows the audio scripts instead of the stock-only RGB list. + ****************************************************************************/ + +void HUEMatrix_Test::hueCacheOffersAudioScriptsAfterStartupStyleLoad() +{ + // Same call shape as qmlui/app.cpp and ui/src/app.cpp: HSV directory with + // the HSV contract, stock directory without it. + Doc fresh(this); + QVERIFY(fresh.rgbScriptsCache()->load(QDir(INTERNAL_SCRIPTDIR))); + QVERIFY(fresh.hueScriptsCache()->load(QDir(INTERNAL_HUESCRIPTDIR), true)); + QVERIFY(fresh.hueScriptsCache()->load(QDir(INTERNAL_SCRIPTDIR), false)); + + QStringList hueList = HUEMatrix::availableAlgorithms(&fresh); + QStringList rgbList = RGBAlgorithm::algorithms(&fresh); + + // This is the regression AC14 names: before the editor was re-pointed it + // returned rgbList, which carries no audio script at all. + QStringList audioOnly; + foreach (QString name, fresh.hueScriptsCache()->hsvNames()) + { + QVERIFY2(hueList.contains(name), qPrintable(name)); + if (rgbList.contains(name) == false) + audioOnly << name; + } + QVERIFY2(audioOnly.isEmpty() == false, + "the two lists are identical - re-pointing the editor would be a no-op"); + + // ...and the stock scripts stay available to HUEMatrix too. + foreach (QString name, fresh.rgbScriptsCache()->names()) + QVERIFY2(hueList.contains(name), qPrintable(name)); +} + +/**************************************************************************** + * AC15: an RGBMatrix whose workspace names a relocated audio script must not + * load silently to nothing. + ****************************************************************************/ + +void HUEMatrix_Test::unavailableAlgorithmWarnsOnWorkspaceLoad() +{ + QStringList hsv = m_doc->hueScriptsCache()->hsvNames(); + QVERIFY(hsv.isEmpty() == false); + QString missing = hsv.first(); + QVERIFY2(m_doc->rgbScriptsCache()->names().contains(missing) == false, + "picked a script that IS available to RGBMatrix"); + + QString xml = QString( + "" + "" + "%1" + "").arg(missing); + + QBuffer buffer; + buffer.setData(xml.toUtf8()); + buffer.open(QIODevice::ReadOnly); + QXmlStreamReader reader(&buffer); + reader.readNextStartElement(); + + // Both halves of the warning: the script name from the cache, the function + // name from the loader. QTest::ignoreMessage fails if it is never emitted. + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression(QString("RGB script \"%1\" is not available").arg(missing))); + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression("Function \"Broken\".*lost its algorithm")); + + QVERIFY(Function::loader(reader, m_doc)); + + Function *loaded = m_doc->function(7); + QVERIFY(loaded != NULL); + RGBMatrix *matrix = qobject_cast (loaded); + QVERIFY(matrix != NULL); + QVERIFY(matrix->algorithm() == NULL); + + // Control: an available algorithm must not warn. + QString ok = QString( + "" + "" + "Stripes" + ""); + QBuffer okBuffer; + okBuffer.setData(ok.toUtf8()); + okBuffer.open(QIODevice::ReadOnly); + QXmlStreamReader okReader(&okBuffer); + okReader.readNextStartElement(); + QVERIFY(Function::loader(okReader, m_doc)); + QVERIFY(qobject_cast (m_doc->function(8))->algorithm() != NULL); +} + +/**************************************************************************** + * AC17: the two function types must be visually distinguishable. + ****************************************************************************/ + +void HUEMatrix_Test::hueMatrixHasItsOwnIcon() +{ + // The engine test binary does not compile the icon .qrc in, so QIcon + // renders nothing here. Assert the shipped artifacts instead: a distinct + // SVG, registered, and actually referenced by both UI entry points. + const QString src = QStringLiteral(QLCPLUS_SOURCE_DIR); + QFile svg(src + "/resources/icons/svg/huematrix.svg"); + QVERIFY2(svg.exists(), qPrintable(QFileInfo(svg).absoluteFilePath())); + QVERIFY(svg.open(QIODevice::ReadOnly)); + QByteArray hueSvg = svg.readAll(); + svg.close(); + QVERIFY(hueSvg.contains(" refs = { + { "/resources/icons/svg/svgicons.qrc", "huematrix.svg" }, + { "/qmlui/functionmanager.cpp", "HUEMatrixType: return \"qrc:/huematrix.svg\"" }, + { "/qmlui/qml/fixturesfunctions/AddFunctionMenu.qml", "qrc:/huematrix.svg" }, + }; + + foreach (const Ref &ref, refs) + { + QFile f(src + ref.path); + QVERIFY2(f.open(QIODevice::ReadOnly), ref.path); + QVERIFY2(f.readAll().contains(ref.needle), ref.path); + f.close(); + } +} + +/**************************************************************************** + * AC18: every UI site that shows a HUEMatrix icon must show the HUE icon. + * + * The AC17 test above checks three hand-picked sites. That let + * FunctionManager.qml's "HUE Matrices" filter button keep "qrc:/rgbmatrix.svg" + * unnoticed. This test instead enumerates *every* function-type icon reference + * in the UI sources and fails on any HUEMatrix one that is not the HUE icon, + * so a newly added site cannot slip through. + ****************************************************************************/ + +/** One `*.svg` reference found in a source file, bound to the function type it + * illustrates. */ +struct IconSite +{ + QString file; + int line; + QString svg; + QString type; /**< "HUEMatrix", "RGBMatrix", ... or empty if unbound */ +}; + +/** Collect every `.svg` reference in `text` and bind each to the + * *nearest* function-type token within kContextLines. Both a C++ switch arm + * and a QML block declare the icon and the type on separate but adjacent + * lines, and the nearest token is the one the icon belongs to. */ +static QVector scanIconSites(const QString &file, const QString &text) +{ + const int kContextLines = 8; + const QStringList lines = text.split('\n'); + static const QRegularExpression svgRef("([A-Za-z0-9_]+\\.svg)"); + static const QRegularExpression typeRef( + "(?:Function::|QLCFunction\\.)([A-Za-z0-9]+)Type\\b"); + + QVector sites; + for (int i = 0; i < lines.count(); i++) + { + QRegularExpressionMatchIterator svgIt = svgRef.globalMatch(lines.at(i)); + if (svgIt.hasNext() == false) + continue; + + // Nearest function-type token, scanning outwards from this line. + QString type; + for (int d = 0; d <= kContextLines && type.isEmpty(); d++) + { + for (int s = 0; s < 2 && type.isEmpty(); s++) + { + const int j = (s == 0) ? i + d : i - d; + if (j < 0 || j >= lines.count()) + continue; + QRegularExpressionMatch m = typeRef.match(lines.at(j)); + if (m.hasMatch()) + type = m.captured(1); + } + } + + while (svgIt.hasNext()) + { + IconSite site; + site.file = file; + site.line = i + 1; + site.svg = svgIt.next().captured(1); + site.type = type; + sites << site; + } + } + return sites; +} + +void HUEMatrix_Test::everyHueMatrixIconSiteUsesTheHueIcon() +{ + // Negative control first: the scanner must actually catch the shape of the + // defect this criterion is about. Without this, a scanner that silently + // binds nothing would pass the real scan below. + const QString prefixSnippet = + "IconButton\n" + "{\n" + " imgSource: \"qrc:/rgbmatrix.svg\"\n" + " checkable: true\n" + " checked: functionManager.functionsFilter & QLCFunction.HUEMatrixType\n" + " tooltip: qsTr(\"HUE Matrices\")\n" + "}\n"; + QVector bad = scanIconSites("", prefixSnippet); + QCOMPARE(bad.count(), 1); + QCOMPARE(bad.first().type, QString("HUEMatrix")); + QCOMPARE(bad.first().svg, QString("rgbmatrix.svg")); + + // Now the real tree. Everything the UI can draw a function icon from. + const QString src = QStringLiteral(QLCPLUS_SOURCE_DIR); + const QStringList roots = { "/qmlui", "/ui/src", "/engine/src", "/resources/icons/svg" }; + const QStringList suffixes = { "qml", "cpp", "h", "qrc" }; + + QVector allSites; + foreach (const QString &root, roots) + { + QDirIterator it(src + root, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) + { + const QString path = it.next(); + if (suffixes.contains(QFileInfo(path).suffix()) == false) + continue; + + QFile f(path); + QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(path)); + allSites += scanIconSites(QDir(src).relativeFilePath(path), + QString::fromUtf8(f.readAll())); + f.close(); + } + } + + // The scan must have found something, otherwise the assertions below are + // vacuous. RGBMatrix is the type this one is most likely to be confused + // with, so require it to be bound too. + QVERIFY2(allSites.count() > 20, qPrintable(QString("only %1 icon references found") + .arg(allSites.count()))); + + QStringList offenders; + QStringList hueSites; + int rgbSites = 0; + foreach (const IconSite &site, allSites) + { + if (site.type == QLatin1String("RGBMatrix")) + { + rgbSites++; + if (site.svg != QLatin1String("rgbmatrix.svg")) + offenders << QString("%1:%2 RGBMatrix -> %3") + .arg(site.file).arg(site.line).arg(site.svg); + } + if (site.type != QLatin1String("HUEMatrix")) + continue; + hueSites << QString("%1:%2 -> %3").arg(site.file).arg(site.line).arg(site.svg); + if (site.svg != QLatin1String("huematrix.svg")) + offenders << QString("%1:%2 HUEMatrix -> %3") + .arg(site.file).arg(site.line).arg(site.svg); + } + + QVERIFY2(offenders.isEmpty(), qPrintable(offenders.join("; "))); + qDebug() << "HUEMatrix icon sites:" << hueSites; + + // Guard against the scan silently narrowing to nothing: the icon switch in + // functionmanager.cpp, the AddFunctionMenu entry and the FunctionManager + // filter button are all type-bound sites. + QVERIFY2(hueSites.count() >= 3, qPrintable(QString("only %1 HUEMatrix icon sites found") + .arg(hueSites.count()))); + QVERIFY2(rgbSites >= 3, qPrintable(QString("only %1 RGBMatrix icon sites found") + .arg(rgbSites))); + + // Two further sites carry no function-type token next to them and so are + // invisible to the scan above. Assert them explicitly rather than leave + // them unchecked. + struct Ref { const char *path; const char *needle; }; + const QVector untyped = { + { "/engine/src/huematrix.cpp", "QIcon(\":/huematrix.svg\")" }, + { "/resources/icons/svg/svgicons.qrc", "alias=\"huematrix.svg\"" }, + }; + foreach (const Ref &ref, untyped) + { + QFile f(src + ref.path); + QVERIFY2(f.open(QIODevice::ReadOnly), ref.path); + QVERIFY2(f.readAll().contains(ref.needle), ref.path); + f.close(); + } +} + +/**************************************************************************** + * AC19: a HUE script must not shadow a built-in algorithm's name. + ****************************************************************************/ + +void HUEMatrix_Test::builtInAudioIsReachableByNameOnHueMatrix() +{ + const QString builtInName = RGBAudio(m_doc).name(); + QCOMPARE(builtInName, QString("Audio Spectrum")); + + // The editor offers this name, so createAlgorithm() must resolve it. + QVERIFY(HUEMatrix::availableAlgorithms(m_doc).contains(builtInName)); + + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, builtInName); + QVERIFY(algo != NULL); + QCOMPARE(algo->type(), RGBAlgorithm::Audio); + QCOMPARE(algo->name(), builtInName); + delete algo; + + // The renamed HSV script is still reachable and is still a Script. + RGBAlgorithm *script = HUEMatrix::createAlgorithm(m_doc, "Audio Spectrum Bars"); + QVERIFY(script != NULL); + QCOMPARE(script->type(), RGBAlgorithm::Script); + delete script; + + // Error case: a name that is neither. + QVERIFY(HUEMatrix::createAlgorithm(m_doc, "No Such Algorithm") == NULL); +} + +void HUEMatrix_Test::noHueScriptShadowsABuiltInName() +{ + // Guard the whole HUE script set, not just the one that was found broken. + QStringList builtIns; + builtIns << RGBPlain(m_doc).name() << RGBText(m_doc).name() + << RGBImage(m_doc).name() << RGBAudio(m_doc).name(); + + QStringList shadowed; + foreach (const QString &name, m_doc->hueScriptsCache()->hsvNames()) + { + if (builtIns.contains(name)) + shadowed << name; + } + QVERIFY2(shadowed.isEmpty(), qPrintable(shadowed.join(", "))); + + // Every built-in must resolve to its built-in type through the HUE factory. + foreach (const QString &name, builtIns) + { + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(algo != NULL, qPrintable(name)); + QVERIFY2(algo->type() != RGBAlgorithm::Script, qPrintable(name)); + delete algo; + } +} + +/**************************************************************************** + * AC20: the destructor's wait for an in-flight async precompute is bounded. + * + * m_precomputedInFlight is raised *before* the job is queued on the JS thread + * and cleared only from inside the queued lambda. If the JS thread goes away + * in between, the flag stays raised forever. Before this fix ~HUEMatrix() spun + * on it with no exit condition and shutdown hung. + ****************************************************************************/ + +void HUEMatrix_Test::destructorGivesUpOnAnAsyncTaskThatNeverClears() +{ + HUEMatrix *mtx = new HUEMatrix(m_doc); + + // Simulate a job that was queued but whose lambda will never run. + mtx->m_precomputedInFlight.storeRelease(1); + QCOMPARE(mtx->m_precomputedInFlight.loadAcquire(), 1); + + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression("async precompute did not drain within")); + + QElapsedTimer timer; + timer.start(); + delete mtx; + const qint64 waited = timer.elapsed(); + + // It must actually have waited (so a real in-flight job still gets its + // chance) but must have terminated. Before the fix this loop had no exit + // condition at all, so the upper bound is deliberately slack - it only has + // to separate "bounded" from "never returns". + QVERIFY2(waited >= HUEMatrix::precomputeDrainTimeoutMs / 2, + qPrintable(QString("gave up after only %1ms").arg(waited))); + QVERIFY2(waited < HUEMatrix::precomputeDrainTimeoutMs * 20, + qPrintable(QString("took %1ms, bound is %2ms") + .arg(waited).arg(HUEMatrix::precomputeDrainTimeoutMs))); +} + +void HUEMatrix_Test::destructorReturnsImmediatelyWhenNothingIsInFlight() +{ + HUEMatrix *mtx = new HUEMatrix(m_doc); + QCOMPARE(mtx->m_precomputedInFlight.loadAcquire(), 0); + + QElapsedTimer timer; + timer.start(); + delete mtx; + + // The common path must not pay the timeout. + QVERIFY2(timer.elapsed() < HUEMatrix::precomputeDrainTimeoutMs / 4, + qPrintable(QString("idle teardown took %1ms").arg(timer.elapsed()))); +} + +/**************************************************************************** + * AC24: the async precompute path. + * + * kickAsyncRgbMap() queues an rgbMap() onto the JS thread and stashes the + * result for the next tick; consumePrecomputedMap() takes it only if nothing + * that would invalidate it moved in the meantime. Neither had any test. + ****************************************************************************/ + +/** Wait (bounded) for the queued JS-thread task to clear the in-flight flag. */ +static bool waitForPrecomputeDrain(HUEMatrix *mtx, int timeoutMs = 60000) +{ + QElapsedTimer timer; + timer.start(); + while (mtx->m_precomputedInFlight.loadAcquire() != 0) + { + if (timer.elapsed() > timeoutMs) + return false; + QThread::msleep(1); + } + return true; +} + +void HUEMatrix_Test::asyncPrecomputeProducesAConsumableMap() +{ + const QString name = m_doc->hueScriptsCache()->hsvNames().first(); + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY2(algo != NULL, qPrintable(name)); + QCOMPARE(algo->type(), RGBAlgorithm::Script); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(algo); + mtx.m_runAlgorithm = algo; + + const QSize algoSize(5, 4); + const uint color = 0x0000FF00; + const int step = 0; + const quint32 gen = mtx.m_currentGeneration.loadAcquire(); + + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 0); + QCOMPARE(mtx.m_precomputedInFlight.loadAcquire(), 0); + + mtx.kickAsyncRgbMap(algo, algoSize, color, step, gen); + QVERIFY2(waitForPrecomputeDrain(&mtx), "async precompute never drained"); + + // The task ran to completion and published a frame. + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 1); + QCOMPARE(mtx.m_precomputedGeneration, gen); + QCOMPARE(mtx.m_precomputedStep, step); + QCOMPARE(mtx.m_precomputedColor, color); + QCOMPARE(mtx.m_precomputedAlgoSize, algoSize); + QCOMPARE(mtx.m_precomputedMap.size(), algoSize.height()); + + // Consume-hit: the frame moves into the step handler and the slot frees. + mtx.m_stepHandler->m_map = RGBMap(); + QCOMPARE(mtx.consumePrecomputedMap(algoSize, color, step, gen), true); + QCOMPARE(mtx.m_stepHandler->m_map.size(), algoSize.height()); + QCOMPARE(mtx.m_stepHandler->m_map.first().size(), algoSize.width()); + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 0); + + // Nothing left to take. + QCOMPARE(mtx.consumePrecomputedMap(algoSize, color, step, gen), false); + + mtx.m_runAlgorithm = NULL; +} + +void HUEMatrix_Test::asyncPrecomputeIsThrottledToOneTaskInFlight() +{ + const QString name = m_doc->hueScriptsCache()->hsvNames().first(); + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY(algo != NULL); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(algo); + mtx.m_runAlgorithm = algo; + + // Pretend a task is already queued. The kick must decline rather than + // stack a second one. + mtx.m_precomputedInFlight.storeRelease(1); + mtx.kickAsyncRgbMap(algo, QSize(5, 4), 0x0000FF00, 0, mtx.m_currentGeneration.loadAcquire()); + + QThread::msleep(50); + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 0); + + // Release the flag by hand; the destructor must not have to wait it out. + mtx.m_precomputedInFlight.storeRelease(0); + mtx.m_runAlgorithm = NULL; +} + +void HUEMatrix_Test::asyncPrecomputeIsSkippedForNonScriptAlgorithms() +{ + // Built-ins render inline and cheaply, so there is nothing to offload. + RGBAlgorithm *plain = HUEMatrix::createAlgorithm(m_doc, "Plain Color"); + QVERIFY(plain != NULL); + QVERIFY(plain->type() != RGBAlgorithm::Script); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(plain); + mtx.m_runAlgorithm = plain; + + mtx.kickAsyncRgbMap(plain, QSize(5, 4), 0x0000FF00, 0, + mtx.m_currentGeneration.loadAcquire()); + + QCOMPARE(mtx.m_precomputedInFlight.loadAcquire(), 0); + QThread::msleep(50); + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 0); + + // Error case: a null algorithm must not raise the flag either. + mtx.kickAsyncRgbMap(NULL, QSize(5, 4), 0x0000FF00, 0, + mtx.m_currentGeneration.loadAcquire()); + QCOMPARE(mtx.m_precomputedInFlight.loadAcquire(), 0); + + mtx.m_runAlgorithm = NULL; +} + +void HUEMatrix_Test::precomputedMapIsRejectedWhenTheGenerationMoved() +{ + const QString name = m_doc->hueScriptsCache()->hsvNames().first(); + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY(algo != NULL); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(algo); + mtx.m_runAlgorithm = algo; + + const QSize algoSize(5, 4); + const uint color = 0x0000FF00; + const quint32 gen = mtx.m_currentGeneration.loadAcquire(); + + mtx.kickAsyncRgbMap(algo, algoSize, color, 0, gen); + QVERIFY2(waitForPrecomputeDrain(&mtx), "async precompute never drained"); + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 1); + + // Any of these four moving must invalidate the stored frame. + const RGBMap sentinel(1, RGBMap::value_type(1, 0x00ABCDEF)); + mtx.m_stepHandler->m_map = sentinel; + + QCOMPARE(mtx.consumePrecomputedMap(algoSize, color, 0, gen + 1), false); + QCOMPARE(mtx.m_stepHandler->m_map, sentinel); + // A miss still frees the slot, so re-arm before each further probe. + mtx.m_precomputedReady.storeRelease(1); + QCOMPARE(mtx.consumePrecomputedMap(algoSize, color, 7, gen), false); + mtx.m_precomputedReady.storeRelease(1); + QCOMPARE(mtx.consumePrecomputedMap(algoSize, 0x00FF0000, 0, gen), false); + mtx.m_precomputedReady.storeRelease(1); + QCOMPARE(mtx.consumePrecomputedMap(QSize(9, 9), color, 0, gen), false); + QCOMPARE(mtx.m_stepHandler->m_map, sentinel); + + // ...and the matching call still succeeds afterwards. + mtx.m_precomputedReady.storeRelease(1); + QCOMPARE(mtx.consumePrecomputedMap(algoSize, color, 0, gen), true); + QVERIFY(mtx.m_stepHandler->m_map != sentinel); + + mtx.m_runAlgorithm = NULL; +} + +void HUEMatrix_Test::inFlightPrecomputeIsDiscardedWhenInvalidatedMidFlight() +{ + const QString name = m_doc->hueScriptsCache()->hsvNames().first(); + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, name); + QVERIFY(algo != NULL); + + HUEMatrix mtx(m_doc); + mtx.setAlgorithm(algo); + mtx.m_runAlgorithm = algo; + + // Kick with a generation that is already stale. The queued task re-checks + // the generation before computing and must drop the work. + const quint32 stale = mtx.m_currentGeneration.loadAcquire() - 1; + mtx.kickAsyncRgbMap(algo, QSize(5, 4), 0x0000FF00, 0, stale); + + QVERIFY2(waitForPrecomputeDrain(&mtx), "stale async precompute never drained"); + QCOMPARE(mtx.m_precomputedReady.loadAcquire(), 0); + QCOMPARE(mtx.consumePrecomputedMap(QSize(5, 4), 0x0000FF00, 0, stale), false); + + mtx.m_runAlgorithm = NULL; +} + +/**************************************************************************** + * AC21: a fork-only tag is dropped when its RGBMatrix is + * loaded by restored-upstream code, and the user is told. + * + * engine/src/rgbmatrix.cpp is a sealed byte-identical upstream artifact, so + * the warning is upstream's own "Unknown RGB matrix tag". This test pins that + * behaviour so a future upstream refresh cannot silence it. + ****************************************************************************/ +void HUEMatrix_Test::forkOnlyAudioProfileIdTagWarnsOnRGBMatrixLoad() +{ + QString xml = + "" + "" + "Stripes" + "3" + ""; + + QBuffer buffer; + buffer.setData(xml.toUtf8()); + buffer.open(QIODevice::ReadOnly); + QXmlStreamReader reader(&buffer); + reader.readNextStartElement(); + + // ignoreMessage fails the test if the warning is never emitted. + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression("Unknown RGB matrix tag.*AudioProfileID")); + + QVERIFY(Function::loader(reader, m_doc)); + + // The function still loads and keeps its algorithm; only the fork-only + // setting is lost. + RGBMatrix *matrix = qobject_cast (m_doc->function(21)); + QVERIFY(matrix != NULL); + QVERIFY(matrix->algorithm() != NULL); + + // Control: the same function without the fork-only tag must not warn. + QString clean = + "" + "" + "Stripes" + ""; + QBuffer cleanBuffer; + cleanBuffer.setData(clean.toUtf8()); + cleanBuffer.open(QIODevice::ReadOnly); + QXmlStreamReader cleanReader(&cleanBuffer); + cleanReader.readNextStartElement(); + QVERIFY(Function::loader(cleanReader, m_doc)); + QVERIFY(qobject_cast (m_doc->function(22))->algorithm() != NULL); +} + +/**************************************************************************** + * AC23: while a HUEMatrix runs an audio algorithm the map is recomputed on + * every tick, not only when the step changes. + * + * Observed, not asserted from usesAudio(): the previous frame's map is + * poisoned with a sentinel and write() is driven on a mid-step tick, where a + * non-audio algorithm is required to leave the sentinel untouched. + ****************************************************************************/ +void HUEMatrix_Test::audioAlgorithmRecomputesTheMapEveryTick() +{ + FixtureGroup *grp = new FixtureGroup(m_doc); + grp->setName("AC23 Group"); + grp->setSize(QSize(3, 3)); + m_doc->addFixtureGroup(grp); + + MasterTimer timer(m_doc); + QList universes = m_doc->inputOutputMap()->claimUniverses(); + m_doc->inputOutputMap()->releaseUniverses(false); + + const uint sentinel = 0x00ABCDEF; + + // drive() runs one tick that is NOT the first tick of a step, so the + // step-change and first-tick arms of the recompute predicate are false and + // only usesAudio() can trigger a recompute. + struct Driver + { + static bool mapWasRecomputed(HUEMatrix &mtx, MasterTimer *timer, + QList universes, uint sentinel) + { + mtx.m_stepHandler->m_map = RGBMap(3, QVector(3, sentinel)); + mtx.write(timer, universes); + const RGBMap &m = mtx.m_stepHandler->m_map; + for (int y = 0; y < m.size(); y++) + for (int x = 0; x < m[y].size(); x++) + if (m[y][x] != sentinel) + return true; + return false; + } + }; + + // --- audio algorithm: recomputes every tick + RGBAlgorithm *audio = HUEMatrix::createAlgorithm(m_doc, "Audio Spectrum"); + QVERIFY(audio != NULL); + QCOMPARE(audio->type(), RGBAlgorithm::Audio); + QVERIFY(audio->usesAudio()); + + HUEMatrix audioMtx(m_doc); + audioMtx.setFixtureGroup(grp->id()); + audioMtx.setAlgorithm(audio); + audioMtx.setBeatEffect(HUEMatrix::BeatEffectOff); + audioMtx.preRun(&timer); + audioMtx.write(&timer, universes); // first tick of the step + QVERIFY2(Driver::mapWasRecomputed(audioMtx, &timer, universes, sentinel), + "audio algorithm did not recompute on a mid-step tick"); + QVERIFY2(Driver::mapWasRecomputed(audioMtx, &timer, universes, sentinel), + "audio algorithm recomputed once but not on the following tick"); + audioMtx.postRun(&timer, universes); + + // --- control: a non-audio script must NOT recompute on a mid-step tick + RGBAlgorithm *plain = HUEMatrix::createAlgorithm(m_doc, "Stripes"); + QVERIFY(plain != NULL); + QVERIFY(plain->usesAudio() == false); + + HUEMatrix plainMtx(m_doc); + plainMtx.setFixtureGroup(grp->id()); + plainMtx.setAlgorithm(plain); + plainMtx.setBeatEffect(HUEMatrix::BeatEffectOff); + plainMtx.preRun(&timer); + plainMtx.write(&timer, universes); // first tick of the step + QVERIFY2(Driver::mapWasRecomputed(plainMtx, &timer, universes, sentinel) == false, + "non-audio algorithm recomputed on a mid-step tick"); + plainMtx.postRun(&timer, universes); +} + +QTEST_MAIN(HUEMatrix_Test) \ No newline at end of file diff --git a/engine/test/huematrix/huematrix_test.h b/engine/test/huematrix/huematrix_test.h new file mode 100644 index 0000000000..5a4d2ffe3d --- /dev/null +++ b/engine/test/huematrix/huematrix_test.h @@ -0,0 +1,110 @@ +/* + Q Light Controller Plus - Unit test + huematrix_test.h + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUEMATRIX_TEST_H +#define HUEMATRIX_TEST_H + +#include + +class Doc; + +class HUEMatrix_Test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + + /* AC1 */ + void functionType(); + + /* AC5 */ + void audioScriptsAreNotOfferedToRGBMatrix(); + void hueMatrixOffersAllAudioScripts(); + + /* AC4 */ + void hsvScriptProducesFiniteNonUniformMap(); + + /* AC6 */ + void upstreamScriptStillWorksOnHueScript(); + + /* AC7 */ + void forkPropertyRoundTripsInMemory_data(); + void forkPropertyRoundTripsInMemory(); + + /* AC8 */ + void forkPropertiesSurviveXmlRoundTrip(); + void unknownXmlValuesFallBackToDefaults(); + + /* AC9 */ + void unavailableAlgorithmIsRejected(); + + /* AC11 */ + void scriptPropertyAttrDoesNotCollide(); + + /* AC12 */ + void hueMatrixSupportsScriptPropertyAttributes(); + void hueMatrixSupportsForkAttributes(); + + /* built-in algorithm edge case */ + void builtInAlgorithmStillRunsTransformPipeline(); + + /* AC13 */ + void hsvScriptRgbMapIsSafeFromAnotherThread(); + + /* AC16 */ + void builtInAudioAlgorithmReportsUsesAudio(); + + /* AC14 */ + void hueCacheOffersAudioScriptsAfterStartupStyleLoad(); + + /* AC15 */ + void unavailableAlgorithmWarnsOnWorkspaceLoad(); + + /* AC17 */ + void hueMatrixHasItsOwnIcon(); + + /* AC18 */ + void everyHueMatrixIconSiteUsesTheHueIcon(); + + /* AC19 */ + void builtInAudioIsReachableByNameOnHueMatrix(); + void noHueScriptShadowsABuiltInName(); + + /* AC20 */ + void destructorGivesUpOnAnAsyncTaskThatNeverClears(); + void destructorReturnsImmediatelyWhenNothingIsInFlight(); + + /* AC24 */ + void asyncPrecomputeProducesAConsumableMap(); + void asyncPrecomputeIsThrottledToOneTaskInFlight(); + void asyncPrecomputeIsSkippedForNonScriptAlgorithms(); + void precomputedMapIsRejectedWhenTheGenerationMoved(); + void inFlightPrecomputeIsDiscardedWhenInvalidatedMidFlight(); + + /* AC21 */ + void forkOnlyAudioProfileIdTagWarnsOnRGBMatrixLoad(); + + /* AC23 */ + void audioAlgorithmRecomputesTheMapEveryTick(); + +private: + Doc *m_doc; +}; + +#endif diff --git a/engine/test/huematrix/test.sh b/engine/test/huematrix/test.sh new file mode 100755 index 0000000000..d7a8e3c919 --- /dev/null +++ b/engine/test/huematrix/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export LD_LIBRARY_PATH=../../src:$LD_LIBRARY_PATH +export DYLD_FALLBACK_LIBRARY_PATH=../../src +./huematrix_test diff --git a/mcp/test/rgb_transform_test.cpp b/mcp/test/rgb_transform_test.cpp index 4e25d2a974..18e7f69e0b 100644 --- a/mcp/test/rgb_transform_test.cpp +++ b/mcp/test/rgb_transform_test.cpp @@ -19,7 +19,7 @@ #include #include "rgb_transform_test.h" -#include "rgbmatrix.h" +#include "huematrix.h" #include "doc.h" // Helper to create a test map with unique values per pixel @@ -47,7 +47,7 @@ void RGBTransform_Test::rotation0_identity() QSize srcSize(4, 2); QSize dstSize(4, 2); - RGBMatrix::applyTransforms(map, srcSize, dstSize, 0, 0, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, srcSize, dstSize, 0, 0, HUEMatrix::MirrorFlip); QCOMPARE(map.size(), 2); QCOMPARE(map[0].size(), (uint)4); @@ -69,7 +69,7 @@ void RGBTransform_Test::rotation90_nonSquare() QSize srcSize(2, 4); // what the algorithm got QSize dstSize(4, 2); // physical grid - RGBMatrix::applyTransforms(src, srcSize, dstSize, 1, 0, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(src, srcSize, dstSize, 1, 0, HUEMatrix::MirrorFlip); // After 90° CW: dst(dx,dy) = src(dy, sh-1-dx) where sh=4 // dst[0][0] = src(0, 3) = src[3][0] = 401 @@ -100,7 +100,7 @@ void RGBTransform_Test::rotation180_nonSquare() QSize srcSize(4, 2); QSize dstSize(4, 2); - RGBMatrix::applyTransforms(map, srcSize, dstSize, 2, 0, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, srcSize, dstSize, 2, 0, HUEMatrix::MirrorFlip); // 180°: dst(dx,dy) = src(W-1-dx, H-1-dy) // dst[0][0] = src[1][3] = 204 @@ -120,7 +120,7 @@ void RGBTransform_Test::rotation270_nonSquare() QSize srcSize(2, 4); QSize dstSize(4, 2); - RGBMatrix::applyTransforms(src, srcSize, dstSize, 3, 0, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(src, srcSize, dstSize, 3, 0, HUEMatrix::MirrorFlip); // 270° CW: dst(dx,dy) = src(sw-1-dy, dx) where sw=2 // dst[0][0] = src(1, 0) = src[0][1] = 102 @@ -157,8 +157,8 @@ void RGBTransform_Test::rotation_roundTrip() // Actually, in the engine flow, each rotation pass swaps dims. // 4 consecutive 90° rotations = identity. // Let's just do 180+180 = identity on the same map: - RGBMatrix::applyTransforms(map, QSize(4, 2), QSize(4, 2), 2, 0, RGBMatrix::MirrorFlip); - RGBMatrix::applyTransforms(map, QSize(4, 2), QSize(4, 2), 2, 0, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, QSize(4, 2), QSize(4, 2), 2, 0, HUEMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, QSize(4, 2), QSize(4, 2), 2, 0, HUEMatrix::MirrorFlip); for (int y = 0; y < 2; y++) for (int x = 0; x < 4; x++) @@ -170,7 +170,7 @@ void RGBTransform_Test::mirrorHorizontal_flip() RGBMap map = makeTestMap(4, 2); QSize s(4, 2); - RGBMatrix::applyTransforms(map, s, s, 0, 1, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, s, s, 0, 1, HUEMatrix::MirrorFlip); // Flip: left half copied to right (top-left is source quadrant) // map[0] was {101, 102, 103, 104} → left half: 101, 102 @@ -186,7 +186,7 @@ void RGBTransform_Test::mirrorVertical_flip() RGBMap map = makeTestMap(4, 2); QSize s(4, 2); - RGBMatrix::applyTransforms(map, s, s, 0, 2, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(map, s, s, 0, 2, HUEMatrix::MirrorFlip); // Vertical flip: top half copied to bottom // With H=2: only 1 row in top half (y < 2/2 = 1), bottom row gets top row @@ -209,7 +209,7 @@ void RGBTransform_Test::mirrorHorizontal_max() map[0][3] = 0x0000FF; // Blue right QSize s(4, 1); - RGBMatrix::applyTransforms(map, s, s, 0, 1, RGBMatrix::MirrorMax); + HUEMatrix::applyTransforms(map, s, s, 0, 1, HUEMatrix::MirrorMax); // Max blend: each pair gets max per channel // (0,0)+(0,3): max(FF,00)=FF, max(00,00)=00, max(00,FF)=FF → 0xFF00FF @@ -229,7 +229,7 @@ void RGBTransform_Test::mirrorVertical_max() map[1][0] = 0x0000FF; // Blue bottom QSize s(1, 2); - RGBMatrix::applyTransforms(map, s, s, 0, 2, RGBMatrix::MirrorMax); + HUEMatrix::applyTransforms(map, s, s, 0, 2, HUEMatrix::MirrorMax); QCOMPARE(map[0][0], 0xFF00FFu); QCOMPARE(map[1][0], 0xFF00FFu); @@ -242,8 +242,8 @@ void RGBTransform_Test::mirrorHorizontalAndVertical_differ() RGBMap mapV = makeTestMap(4, 3); QSize s(4, 3); - RGBMatrix::applyTransforms(mapH, s, s, 0, 1, RGBMatrix::MirrorFlip); - RGBMatrix::applyTransforms(mapV, s, s, 0, 2, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(mapH, s, s, 0, 1, HUEMatrix::MirrorFlip); + HUEMatrix::applyTransforms(mapV, s, s, 0, 2, HUEMatrix::MirrorFlip); // They should differ: H mirrors columns, V mirrors rows bool differ = false; @@ -261,7 +261,7 @@ void RGBTransform_Test::rotation90_withMirror() QSize srcSize(2, 4); QSize dstSize(4, 2); - RGBMatrix::applyTransforms(src, srcSize, dstSize, 1, 1, RGBMatrix::MirrorFlip); + HUEMatrix::applyTransforms(src, srcSize, dstSize, 1, 1, HUEMatrix::MirrorFlip); // After 90° CW, the map is 4 wide × 2 tall QCOMPARE(src.size(), 2); @@ -280,7 +280,7 @@ void RGBTransform_Test::settersClamping() { // Test that setters clamp values properly Doc doc(nullptr); - RGBMatrix matrix(&doc); + HUEMatrix matrix(&doc); matrix.setRotation(5); QCOMPARE(matrix.rotation(), 1); // 5 & 3 = 1 @@ -291,22 +291,22 @@ void RGBTransform_Test::settersClamping() matrix.setMirror(7); QCOMPARE(matrix.mirror(), 3); // 7 & 3 = 3 - matrix.setMirrorBlend(RGBMatrix::MirrorBlend(99)); - QCOMPARE(matrix.mirrorBlend(), RGBMatrix::MirrorFlip); // out of range → default + matrix.setMirrorBlend(HUEMatrix::MirrorBlend(99)); + QCOMPARE(matrix.mirrorBlend(), HUEMatrix::MirrorFlip); // out of range → default } void RGBTransform_Test::mirrorBlend_stringConversion() { - QCOMPARE(RGBMatrix::mirrorBlendToString(RGBMatrix::MirrorFlip), QStringLiteral("Flip")); - QCOMPARE(RGBMatrix::mirrorBlendToString(RGBMatrix::MirrorMax), QStringLiteral("Max")); - QCOMPARE(RGBMatrix::mirrorBlendToString(RGBMatrix::MirrorAverage), QStringLiteral("Average")); - QCOMPARE(RGBMatrix::mirrorBlendToString(RGBMatrix::MirrorAdditive), QStringLiteral("Additive")); - - QCOMPARE(RGBMatrix::stringToMirrorBlend("Flip"), RGBMatrix::MirrorFlip); - QCOMPARE(RGBMatrix::stringToMirrorBlend("Max"), RGBMatrix::MirrorMax); - QCOMPARE(RGBMatrix::stringToMirrorBlend("Average"), RGBMatrix::MirrorAverage); - QCOMPARE(RGBMatrix::stringToMirrorBlend("Additive"), RGBMatrix::MirrorAdditive); - QCOMPARE(RGBMatrix::stringToMirrorBlend("Unknown"), RGBMatrix::MirrorFlip); + QCOMPARE(HUEMatrix::mirrorBlendToString(HUEMatrix::MirrorFlip), QStringLiteral("Flip")); + QCOMPARE(HUEMatrix::mirrorBlendToString(HUEMatrix::MirrorMax), QStringLiteral("Max")); + QCOMPARE(HUEMatrix::mirrorBlendToString(HUEMatrix::MirrorAverage), QStringLiteral("Average")); + QCOMPARE(HUEMatrix::mirrorBlendToString(HUEMatrix::MirrorAdditive), QStringLiteral("Additive")); + + QCOMPARE(HUEMatrix::stringToMirrorBlend("Flip"), HUEMatrix::MirrorFlip); + QCOMPARE(HUEMatrix::stringToMirrorBlend("Max"), HUEMatrix::MirrorMax); + QCOMPARE(HUEMatrix::stringToMirrorBlend("Average"), HUEMatrix::MirrorAverage); + QCOMPARE(HUEMatrix::stringToMirrorBlend("Additive"), HUEMatrix::MirrorAdditive); + QCOMPARE(HUEMatrix::stringToMirrorBlend("Unknown"), HUEMatrix::MirrorFlip); } QTEST_MAIN(RGBTransform_Test) diff --git a/mcp/tools/conversions.h b/mcp/tools/conversions.h index 536e178a23..f591a1074f 100644 --- a/mcp/tools/conversions.h +++ b/mcp/tools/conversions.h @@ -42,6 +42,7 @@ #include "efx.h" #include "scenevalue.h" #include "rgbmatrix.h" +#include "huematrix.h" #include "rgbalgorithm.h" #include "fixturegroup.h" #include "universe.h" @@ -223,12 +224,21 @@ inline std::string rgbAlgorithmTypeToString(RGBAlgorithm::Type type) } } +// Pure function: control mode name, honouring HUEMatrix' additive modes +inline std::string controlModeName(RGBMatrix *matrix) +{ + if (qobject_cast(matrix) != NULL) + return HUEMatrix::controlModeToString(matrix->controlMode()).toStdString(); + return RGBMatrix::controlModeToString(matrix->controlMode()).toStdString(); +} + // Convert an RGBMatrix to detailed JSON inline Json rgbMatrixToJson(RGBMatrix *matrix) { Json entry; entry["id"] = (int)matrix->id(); entry["name"] = matrix->name().toStdString(); + entry["type"] = Function::typeToString(matrix->type()).toStdString(); if (!matrix->path().isEmpty()) entry["path"] = matrix->path().toStdString(); entry["fixtureGroupID"] = (int)matrix->fixtureGroup(); @@ -253,7 +263,7 @@ inline Json rgbMatrixToJson(RGBMatrix *matrix) entry["runOrder"] = Function::runOrderToString(matrix->runOrder()).toStdString(); entry["direction"] = Function::directionToString(matrix->direction()).toStdString(); - entry["controlMode"] = RGBMatrix::controlModeToString(matrix->controlMode()).toStdString(); + entry["controlMode"] = controlModeName(matrix); entry["blendMode"] = Universe::blendModeToString(matrix->blendMode()).toStdString(); // Algorithm @@ -279,23 +289,26 @@ inline Json rgbMatrixToJson(RGBMatrix *matrix) entry["stepsCount"] = matrix->stepsCount(); - // Rotation & Mirror - if (matrix->rotation() != 0) - entry["rotation"] = matrix->rotation() * 90; // 0, 90, 180, 270 - if (matrix->mirror() != 0) + // Rotation, Mirror and Beat Transform are HUEMatrix-only + HUEMatrix *hue = qobject_cast(matrix); + if (hue != NULL) { - static const char *mirrorNames[] = {"Off", "Horizontal", "Vertical", "Both"}; - entry["mirror"] = mirrorNames[matrix->mirror() & 3]; - } - if (matrix->mirrorBlend() != RGBMatrix::MirrorFlip) - entry["mirrorBlend"] = RGBMatrix::mirrorBlendToString(matrix->mirrorBlend()).toStdString(); + if (hue->rotation() != 0) + entry["rotation"] = hue->rotation() * 90; // 0, 90, 180, 270 + if (hue->mirror() != 0) + { + static const char *mirrorNames[] = {"Off", "Horizontal", "Vertical", "Both"}; + entry["mirror"] = mirrorNames[hue->mirror() & 3]; + } + if (hue->mirrorBlend() != HUEMatrix::MirrorFlip) + entry["mirrorBlend"] = HUEMatrix::mirrorBlendToString(hue->mirrorBlend()).toStdString(); - // Beat Transform - if (matrix->beatEffect() != RGBMatrix::BeatEffectOff) - { - entry["beatEffect"] = RGBMatrix::beatEffectToString(matrix->beatEffect()).toStdString(); - entry["beatSelection"] = RGBMatrix::beatSelectionToString(matrix->beatSelection()).toStdString(); - entry["beatOrientation"] = RGBMatrix::beatOrientationToString(matrix->beatOrientation()).toStdString(); + if (hue->beatEffect() != HUEMatrix::BeatEffectOff) + { + entry["beatEffect"] = HUEMatrix::beatEffectToString(hue->beatEffect()).toStdString(); + entry["beatSelection"] = HUEMatrix::beatSelectionToString(hue->beatSelection()).toStdString(); + entry["beatOrientation"] = HUEMatrix::beatOrientationToString(hue->beatOrientation()).toStdString(); + } } return entry; @@ -646,14 +659,14 @@ inline Json functionToJson(Function *fn) entry["direction"] = Function::directionToString(efx->direction()).toStdString(); } } - else if (fn->type() == Function::RGBMatrixType) + else if (fn->type() == Function::RGBMatrixType || fn->type() == Function::HUEMatrixType) { RGBMatrix *matrix = qobject_cast(fn); if (matrix) { entry["fixtureGroupID"] = (int)matrix->fixtureGroup(); entry["tempoType"] = matrix->tempoType() == Function::Beats ? "beats" : "time"; - entry["controlMode"] = RGBMatrix::controlModeToString(matrix->controlMode()).toStdString(); + entry["controlMode"] = controlModeName(matrix); if (matrix->blendMode() != Universe::NormalBlend) entry["blendMode"] = Universe::blendModeToString(matrix->blendMode()).toStdString(); RGBAlgorithm *algo = matrix->algorithm(); diff --git a/mcp/tools/function_tools.cpp b/mcp/tools/function_tools.cpp index c5b14033d7..93aa2f042e 100644 --- a/mcp/tools/function_tools.cpp +++ b/mcp/tools/function_tools.cpp @@ -33,6 +33,7 @@ #include "efx.h" #include "efxfixture.h" #include "rgbmatrix.h" +#include "huematrix.h" #include "rgbalgorithm.h" #include "rgbscriptv4.h" #include "rgbtext.h" @@ -57,6 +58,7 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM "create_scenes", Json{{"type", "object"}, {"properties", { {"items", {{"type", "array"}, {"items", {{"type", "object"}, {"properties", { + {"type", {{"type", "string"}, {"enum", {"RGBMatrix", "HUEMatrix"}}, {"description", "HUEMatrix (default) supports HSV/audio scripts plus rotation, mirror and beat transforms. RGBMatrix is the plain upstream matrix."}}}, {"name", {{"type", "string"}}}, {"path", {{"type", "string"}, {"description", "Folder path (e.g. 'Phase1/Moods'). Creates folders implicitly."}}}, {"fixtureIDs", {{"type", "array"}, {"items", {{"type", "integer"}}}}}, @@ -305,6 +307,7 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM if (!err.empty()) { results.push_back(nlohmann::json::parse(err)); continue; } static const Json kEnums = { + {"type", {{"enum", {"RGBMatrix", "HUEMatrix"}}}}, {"tempoType", {{"enum", {"time", "beats"}}}}, {"runOrder", {{"enum", {"loop", "single", "pingpong", "random"}}}}, {"direction", {{"enum", {"forward", "backward"}}}}, @@ -802,7 +805,7 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM Json results = Json::array(); for (auto &item : args.at("items")) { - auto err = validateFields(item, {"name", "path", "fixtureGroupID", "algorithm", + auto err = validateFields(item, {"name", "type", "path", "fixtureGroupID", "algorithm", "startColor", "endColor", "colors", "duration", "fadeIn", "fadeOut", "tempoType", "runOrder", "direction", "controlMode", "blendMode", "properties", "text", "animationStyle", "rotation", "mirror", "mirrorBlend", @@ -826,7 +829,31 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM if (!err.empty()) { results.push_back(nlohmann::json::parse(err)); continue; } QString name = QString::fromStdString(item.at("name").get()); - Function *existing = mcp::findFunction(doc, name, Function::RGBMatrixType); + // Default to HUEMatrix: it is a superset of RGBMatrix and the only + // type accepting the rotation/mirror/beat fields of this schema. + bool wantHue = true; + if (item.contains("type")) + wantHue = item.at("type").get() != "RGBMatrix"; + + static const char *kHueOnly[] = {"rotation", "mirror", "mirrorBlend", + "beatEffect", "beatSelection", "beatOrientation"}; + if (!wantHue) + { + bool rejected = false; + for (const char *f : kHueOnly) + { + if (item.contains(f)) + { + results.push_back({{"error", std::string(f) + " is only supported on a HUEMatrix"}}); + rejected = true; + break; + } + } + if (rejected) continue; + } + + Function::Type wantType = wantHue ? Function::HUEMatrixType : Function::RGBMatrixType; + Function *existing = mcp::findFunction(doc, name, wantType); RGBMatrix *matrix; bool isNew = false; if (existing) @@ -835,10 +862,11 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM } else { - matrix = new RGBMatrix(doc); + matrix = wantHue ? new HUEMatrix(doc) : new RGBMatrix(doc); matrix->setName(name); isNew = true; } + HUEMatrix *hueMatrix = qobject_cast(matrix); if (item.contains("path")) matrix->setPath(QString::fromStdString(item.at("path").get())); @@ -850,9 +878,14 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM if (item.contains("algorithm")) { QString algoName = QString::fromStdString(item.at("algorithm").get()); - RGBAlgorithm *algo = RGBAlgorithm::algorithm(doc, algoName); - if (algo) - matrix->setAlgorithm(algo); + RGBAlgorithm *algo = hueMatrix != NULL ? HUEMatrix::createAlgorithm(doc, algoName) + : RGBAlgorithm::algorithm(doc, algoName); + if (algo == NULL) + { + results.push_back({{"error", "unknown algorithm: " + algoName.toStdString()}}); + continue; + } + matrix->setAlgorithm(algo); } // Colors: prefer 'colors' array, fall back to startColor/endColor @@ -923,7 +956,9 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM if (item.contains("controlMode")) { QString cm = QString::fromStdString(item.at("controlMode").get()); - matrix->setControlMode(RGBMatrix::stringToControlMode(cm)); + RGBMatrix::ControlMode mode = hueMatrix != NULL ? HUEMatrix::stringToControlMode(cm) + : RGBMatrix::stringToControlMode(cm); + matrix->setControlMode(mode); } // Blend mode @@ -997,44 +1032,46 @@ void registerFunctionTools(fastmcpp::tools::ToolManager &tm, Doc *doc, FunctionM } } - // Rotation & Mirror - if (item.contains("rotation")) - matrix->setRotation((item.at("rotation").get() / 90) & 3); - - if (item.contains("mirror")) + // Rotation, Mirror and Beat Transform (HUEMatrix only) + if (hueMatrix != NULL) { - QString m = QString::fromStdString(item.at("mirror").get()); - if (m.compare("Horizontal", Qt::CaseInsensitive) == 0) - matrix->setMirror(1); - else if (m.compare("Vertical", Qt::CaseInsensitive) == 0) - matrix->setMirror(2); - else if (m.compare("Both", Qt::CaseInsensitive) == 0) - matrix->setMirror(3); - else - matrix->setMirror(0); - } + if (item.contains("rotation")) + hueMatrix->setRotation((item.at("rotation").get() / 90) & 3); - if (item.contains("mirrorBlend")) - { - QString mb = QString::fromStdString(item.at("mirrorBlend").get()); - matrix->setMirrorBlend(RGBMatrix::stringToMirrorBlend(mb)); - } + if (item.contains("mirror")) + { + QString m = QString::fromStdString(item.at("mirror").get()); + if (m.compare("Horizontal", Qt::CaseInsensitive) == 0) + hueMatrix->setMirror(1); + else if (m.compare("Vertical", Qt::CaseInsensitive) == 0) + hueMatrix->setMirror(2); + else if (m.compare("Both", Qt::CaseInsensitive) == 0) + hueMatrix->setMirror(3); + else + hueMatrix->setMirror(0); + } - // Beat Transform - if (item.contains("beatEffect")) - { - QString be = QString::fromStdString(item.at("beatEffect").get()); - matrix->setBeatEffect(RGBMatrix::stringToBeatEffect(be)); - } - if (item.contains("beatSelection")) - { - QString bs = QString::fromStdString(item.at("beatSelection").get()); - matrix->setBeatSelection(RGBMatrix::stringToBeatSelection(bs)); - } - if (item.contains("beatOrientation")) - { - QString bo = QString::fromStdString(item.at("beatOrientation").get()); - matrix->setBeatOrientation(RGBMatrix::stringToBeatOrientation(bo)); + if (item.contains("mirrorBlend")) + { + QString mb = QString::fromStdString(item.at("mirrorBlend").get()); + hueMatrix->setMirrorBlend(HUEMatrix::stringToMirrorBlend(mb)); + } + + if (item.contains("beatEffect")) + { + QString be = QString::fromStdString(item.at("beatEffect").get()); + hueMatrix->setBeatEffect(HUEMatrix::stringToBeatEffect(be)); + } + if (item.contains("beatSelection")) + { + QString bs = QString::fromStdString(item.at("beatSelection").get()); + hueMatrix->setBeatSelection(HUEMatrix::stringToBeatSelection(bs)); + } + if (item.contains("beatOrientation")) + { + QString bo = QString::fromStdString(item.at("beatOrientation").get()); + hueMatrix->setBeatOrientation(HUEMatrix::stringToBeatOrientation(bo)); + } } if (isNew) diff --git a/mcp/tools/query_tools.cpp b/mcp/tools/query_tools.cpp index 8b7c4593d0..87d670fd6c 100644 --- a/mcp/tools/query_tools.cpp +++ b/mcp/tools/query_tools.cpp @@ -942,7 +942,7 @@ void registerQueryTools(fastmcpp::tools::ToolManager &tm, Doc *doc, VCBridge *vc Json results = Json::array(); for (Function *fn : doc->functions()) { - if (fn->type() != Function::RGBMatrixType) continue; + if (fn->type() != Function::RGBMatrixType && fn->type() != Function::HUEMatrixType) continue; RGBMatrix *matrix = qobject_cast(fn); if (!matrix) continue; @@ -994,7 +994,7 @@ void registerQueryTools(fastmcpp::tools::ToolManager &tm, Doc *doc, VCBridge *vc [doc, vcBridge](const Json &) -> Json { return execOnMainThread(doc, [&]() -> Json { int scenes = 0, chasers = 0, sequences = 0, collections = 0; - int rgbMatrices = 0, efx = 0, scripts = 0, shows = 0; + int rgbMatrices = 0, hueMatrices = 0, efx = 0, scripts = 0, shows = 0; int audio = 0, video = 0; for (Function *fn : doc->functions()) { @@ -1006,6 +1006,7 @@ void registerQueryTools(fastmcpp::tools::ToolManager &tm, Doc *doc, VCBridge *vc case Function::SequenceType: sequences++; break; case Function::CollectionType: collections++; break; case Function::RGBMatrixType: rgbMatrices++; break; + case Function::HUEMatrixType: hueMatrices++; break; case Function::EFXType: efx++; break; case Function::ScriptType: scripts++; break; case Function::ShowType: shows++; break; @@ -1037,6 +1038,7 @@ void registerQueryTools(fastmcpp::tools::ToolManager &tm, Doc *doc, VCBridge *vc fns["chasers"] = chasers; fns["collections"] = collections; fns["rgbMatrices"] = rgbMatrices; + fns["hueMatrices"] = hueMatrices; fns["efx"] = efx; fns["scripts"] = scripts; fns["shows"] = shows; @@ -1052,7 +1054,7 @@ void registerQueryTools(fastmcpp::tools::ToolManager &tm, Doc *doc, VCBridge *vc }, std::nullopt, std::string("Lightweight workspace overview. Returns counts of fixtures, fixture groups, universes, " - "functions (grouped by type: scenes, chasers, collections, rgbMatrices, efx, scripts, " + "functions (grouped by type: scenes, chasers, collections, rgbMatrices, hueMatrices, efx, scripts, " "shows, sequences, audio, video), palettes, VC pages, and currently running functions. " "Use as a fast first call before drilling into specific entities."), std::nullopt diff --git a/qmlui/CMakeLists.txt b/qmlui/CMakeLists.txt index f1b8986716..726b15cd5e 100644 --- a/qmlui/CMakeLists.txt +++ b/qmlui/CMakeLists.txt @@ -53,6 +53,7 @@ set(SRC_FILES palettemanager.cpp palettemanager.h previewcontext.cpp previewcontext.h rgbmatrixeditor.cpp rgbmatrixeditor.h + huematrixeditor.cpp huematrixeditor.h sceneeditor.cpp sceneeditor.h scripteditor.cpp scripteditor.h showmanager.cpp showmanager.h diff --git a/qmlui/app.cpp b/qmlui/app.cpp index 62a3f51e23..91b56fa58a 100644 --- a/qmlui/app.cpp +++ b/qmlui/app.cpp @@ -70,6 +70,7 @@ #include "ioplugincache.h" #include "qlcioplugin.h" #include "rgbscriptscache.h" +#include "huescriptscache.h" #include "qlcconfig.h" #include "qlcfile.h" @@ -681,6 +682,14 @@ void App::initDoc() m_doc->rgbScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory()); m_doc->rgbScriptsCache()->load(RGBScriptsCache::userScriptsDirectory()); + /* Load HUE scripts. HUEMatrix offers these plus every stock RGB script, + so the stock directories are registered here too, without the HSV + contract. */ + m_doc->hueScriptsCache()->load(HUEScriptsCache::systemScriptsDirectory(), true); + m_doc->hueScriptsCache()->load(HUEScriptsCache::userScriptsDirectory(), true); + m_doc->hueScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory(), false); + m_doc->hueScriptsCache()->load(RGBScriptsCache::userScriptsDirectory(), false); + /* Load plugins */ #if defined Q_OS_ANDROID QString pluginsPath = QCoreApplication::applicationDirPath(); diff --git a/qmlui/functionmanager.cpp b/qmlui/functionmanager.cpp index 7969434a05..294f515108 100644 --- a/qmlui/functionmanager.cpp +++ b/qmlui/functionmanager.cpp @@ -26,6 +26,8 @@ #include "collectioneditor.h" #include "functionmanager.h" #include "rgbmatrixeditor.h" +#include "huematrixeditor.h" +#include "huematrix.h" #include "treemodelitem.h" #include "chasereditor.h" #include "scripteditor.h" @@ -62,7 +64,7 @@ FunctionManager::FunctionManager(QQuickView *view, Doc *doc, QObject *parent) , m_searchFilter(QString()) { m_sceneCount = m_chaserCount = m_sequenceCount = m_efxCount = 0; - m_collectionCount = m_rgbMatrixCount = m_scriptCount = 0; + m_collectionCount = m_rgbMatrixCount = m_hueMatrixCount = m_scriptCount = 0; m_showCount = m_audioCount = m_videoCount = 0; m_runningCount = 0; m_showRunningOnly = false; @@ -74,6 +76,7 @@ FunctionManager::FunctionManager(QQuickView *view, Doc *doc, QObject *parent) qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "Collection", "Can't create a Collection"); qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "Chaser", "Can't create a Chaser"); qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "RGBMatrix", "Can't create a RGBMatrix"); + qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "HUEMatrix", "Can't create a HUEMatrix"); qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "EFX", "Can't create an EFX"); // register SceneValue to perform QVariant comparisons @@ -369,6 +372,14 @@ quint32 FunctionManager::createFunction(int type, QVariantList fixturesList) emit rgbMatrixCountChanged(); } break; + case Function::HUEMatrixType: + { + f = new HUEMatrix(m_doc); + name = tr("New HUE Matrix"); + m_hueMatrixCount++; + emit hueMatrixCountChanged(); + } + break; case Function::ScriptType: { f = new Script(m_doc); @@ -484,6 +495,7 @@ QString FunctionManager::functionIcon(int type) case Function::CollectionType: return "qrc:/collection.svg"; case Function::ScriptType: return "qrc:/script.svg"; case Function::RGBMatrixType: return "qrc:/rgbmatrix.svg"; + case Function::HUEMatrixType: return "qrc:/huematrix.svg"; case Function::ShowType: return "qrc:/showmanager.svg"; case Function::AudioType: return "qrc:/audio.svg"; case Function::VideoType: return "qrc:/video.svg"; @@ -645,6 +657,7 @@ QString FunctionManager::getEditorResource(int funcID) case Function::EFXType: return "qrc:/EFXEditor.qml"; case Function::CollectionType: return "qrc:/CollectionEditor.qml"; case Function::RGBMatrixType: return "qrc:/RGBMatrixEditor.qml"; + case Function::HUEMatrixType: return "qrc:/HUEMatrixEditor.qml"; case Function::ShowType: return "qrc:/ShowManager.qml"; case Function::ScriptType: return "qrc:/ScriptEditor.qml"; case Function::AudioType: return "qrc:/AudioEditor.qml"; @@ -736,6 +749,11 @@ void FunctionManager::setEditorFunction(quint32 fID, bool requestUI, bool back) m_currentEditor = new RGBMatrixEditor(m_view, m_doc, this); } break; + case Function::HUEMatrixType: + { + m_currentEditor = new HUEMatrixEditor(m_view, m_doc, this); + } + break; case Function::ScriptType: { m_currentEditor = new ScriptEditor(m_view, m_doc, this); @@ -1770,6 +1788,7 @@ void FunctionManager::addFunctionTreeItem(Function *func) case Function::EFXType: m_efxCount++; break; case Function::CollectionType: m_collectionCount++; break; case Function::RGBMatrixType: m_rgbMatrixCount++; break; + case Function::HUEMatrixType: m_hueMatrixCount++; break; case Function::ScriptType: m_scriptCount++; break; case Function::ShowType: m_showCount++; break; case Function::AudioType: m_audioCount++; break; @@ -1806,7 +1825,7 @@ void FunctionManager::updateFunctionsTree() storeExpandedPaths(); m_sceneCount = m_chaserCount = m_sequenceCount = m_efxCount = 0; - m_collectionCount = m_rgbMatrixCount = m_scriptCount = 0; + m_collectionCount = m_rgbMatrixCount = m_hueMatrixCount = m_scriptCount = 0; m_showCount = m_audioCount = m_videoCount = 0; m_functionTree->clear(); diff --git a/qmlui/functionmanager.h b/qmlui/functionmanager.h index 6e843fd48d..c9b04bfe3e 100644 --- a/qmlui/functionmanager.h +++ b/qmlui/functionmanager.h @@ -60,6 +60,7 @@ class FunctionManager final : public QObject Q_PROPERTY(int efxCount READ efxCount NOTIFY efxCountChanged) Q_PROPERTY(int collectionCount READ collectionCount NOTIFY collectionCountChanged) Q_PROPERTY(int rgbMatrixCount READ rgbMatrixCount NOTIFY rgbMatrixCountChanged) + Q_PROPERTY(int hueMatrixCount READ hueMatrixCount NOTIFY hueMatrixCountChanged) Q_PROPERTY(int scriptCount READ scriptCount NOTIFY scriptCountChanged) Q_PROPERTY(int showCount READ showCount NOTIFY showCountChanged) Q_PROPERTY(int audioCount READ audioCount NOTIFY audioCountChanged) @@ -198,6 +199,7 @@ class FunctionManager final : public QObject int efxCount() const { return m_efxCount; } int collectionCount() const { return m_collectionCount; } int rgbMatrixCount() const { return m_rgbMatrixCount; } + int hueMatrixCount() const { return m_hueMatrixCount; } int scriptCount() const { return m_scriptCount; } int showCount() const { return m_showCount; } int audioCount() const { return m_audioCount; } @@ -232,6 +234,7 @@ class FunctionManager final : public QObject void efxCountChanged(); void collectionCountChanged(); void rgbMatrixCountChanged(); + void hueMatrixCountChanged(); void scriptCountChanged(); void showCountChanged(); void audioCountChanged(); @@ -277,7 +280,7 @@ private slots: QString m_searchFilter; int m_sceneCount, m_chaserCount, m_sequenceCount, m_efxCount; - int m_collectionCount, m_rgbMatrixCount, m_scriptCount; + int m_collectionCount, m_rgbMatrixCount, m_hueMatrixCount, m_scriptCount; int m_showCount, m_audioCount, m_videoCount; /** Running functions tracking */ diff --git a/qmlui/huematrixeditor.cpp b/qmlui/huematrixeditor.cpp new file mode 100644 index 0000000000..54e058b61a --- /dev/null +++ b/qmlui/huematrixeditor.cpp @@ -0,0 +1,1084 @@ +/* + Q Light Controller Plus + huematrixeditor.cpp + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include + +#include "huematrixeditor.h" + +#include "qlcfixturehead.h" +#include "huematrix.h" +#include "rgbmatrix.h" +#include "rgbimage.h" +#include "sequence.h" +#include "rgbtext.h" +#include "tardis.h" +#include "scene.h" +#include "doc.h" + +HUEMatrixEditor::HUEMatrixEditor(QQuickView *view, Doc *doc, QObject *parent) + : FunctionEditor(view, doc, parent) + , m_matrix(nullptr) + , m_group(nullptr) + , m_previewTimer(new QTimer(this)) + , m_previewElapsed(0) + , m_previewStepHandler(new RGBMatrixStep()) +{ + m_view->rootContext()->setContextProperty("hueMatrixEditor", this); + + m_gotBeat = false; + connect(m_previewTimer, SIGNAL(timeout()), this, SLOT(slotPreviewTimeout())); + connect(m_doc->masterTimer(), SIGNAL(beat()), this, SLOT(slotBeatReceived())); +} + +HUEMatrixEditor::~HUEMatrixEditor() +{ + m_previewTimer->stop(); + m_view->rootContext()->setContextProperty("hueMatrixEditor", nullptr); + delete m_previewStepHandler; +} + +void HUEMatrixEditor::setFunctionID(quint32 id) +{ + if (id == Function::invalidId()) + { + m_matrix = nullptr; + m_group = nullptr; + return; + } + + m_matrix = qobject_cast(m_doc->function(id)); + if (m_matrix == nullptr) + return; + + m_group = m_doc->fixtureGroup(m_matrix->fixtureGroup()); + + if (m_group == nullptr) + { + if (m_doc->fixtureGroups().count()) + { + m_group = m_doc->fixtureGroups().first(); + m_matrix->setFixtureGroup(m_group->id()); + } + } + + initPreviewData(); + emit previewSizeChanged(); + + FunctionEditor::setFunctionID(id); +} + +int HUEMatrixEditor::fixtureGroup() const +{ + if (m_matrix == nullptr || m_group == nullptr) + return -1; + + return m_group->id(); +} + +void HUEMatrixEditor::setFixtureGroup(int fixtureGroup) +{ + if (m_matrix == nullptr || m_matrix->fixtureGroup() == (quint32)fixtureGroup) + return; + + QMutexLocker locker(&m_previewMutex); + m_previewTimer->stop(); + m_group = m_doc->fixtureGroup(fixtureGroup); + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetFixtureGroup, m_matrix->id(), m_matrix->fixtureGroup(), fixtureGroup); + m_matrix->setFixtureGroup((quint32)fixtureGroup); + emit fixtureGroupChanged(fixtureGroup); + emit previewSizeChanged(); + initPreviewData(); +} + +/************************************************************************ + * Algorithm + ************************************************************************/ + +QStringList HUEMatrixEditor::algorithms() const +{ + return HUEMatrix::availableAlgorithms(m_doc); +} + +int HUEMatrixEditor::algorithmIndex() const +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return -1; + + QStringList algoList = algorithms(); + return algoList.indexOf(m_matrix->algorithm()->name()); +} + +void HUEMatrixEditor::setAlgorithmIndex(int algoIndex) +{ + qDebug() << "Set algorithm:" << algoIndex; + QStringList algoList = algorithms(); + if (algoIndex < 0 || algoIndex >= algorithms().count()) + return; + + RGBAlgorithm *algo = HUEMatrix::createAlgorithm(m_doc, algoList.at(algoIndex)); + if (algo != nullptr) + { + /** if we're setting the same algorithm, then there's nothing to do */ + if (m_matrix->algorithm() != nullptr && m_matrix->algorithm()->name() == algo->name()) + return; + + updateColors(); + + Q_ASSERT(5 == RGBAlgorithmColorDisplayCount); + QVector colors = { + m_matrix->getColor(0), + m_matrix->getColor(1), + m_matrix->getColor(2), + m_matrix->getColor(3), + m_matrix->getColor(4) + }; + algo->setColors(colors); + } + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetAlgorithmIndex, m_matrix->id(), algorithmIndex(), algoIndex); + m_matrix->setAlgorithm(algo); + + initPreviewData(); + + emit algorithmIndexChanged(); + emit algoColorsCountChanged(); + emit algoColorsChanged(); +} + +int HUEMatrixEditor::algoColorsCount() const +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return 0; + + return m_matrix->algorithm()->acceptColors(); +} + +QVariantList HUEMatrixEditor::algoColors() +{ + m_algoColors.clear(); + + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return m_algoColors; + + m_algoColors = QVariantList() + << m_matrix->getColor(0) + << m_matrix->getColor(1) + << m_matrix->getColor(2) + << m_matrix->getColor(3) + << m_matrix->getColor(4); + + return m_algoColors; +} + +QColor HUEMatrixEditor::colorAtIndex(int index) const +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return QColor(); + + return m_matrix->getColor(index); +} + +void HUEMatrixEditor::setColorAtIndex(int index, QColor color) +{ + if (m_matrix == nullptr || m_matrix->getColor(index) == color) + return; + + if (m_matrix->controlMode() != RGBMatrix::ControlModeRgb && + m_matrix->controlMode() != RGBMatrix::ControlModeRgbw && + m_matrix->controlMode() != RGBMatrix::ControlModeRgbwBrighter) + { + // Convert color to grayscale for non-RGB control modes + uchar gray = qGray(color.rgb()); + color = QColor(gray, gray, gray); + } + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetColor1 + index, m_matrix->id(), m_matrix->getColor(index), color); + m_matrix->setColor(index, color); + if (index < 2) + m_previewStepHandler->calculateColorDelta(m_matrix->getColor(0), m_matrix->getColor(1), m_matrix->algorithm()); + + emit algoColorsChanged(); +} + +void HUEMatrixEditor::resetColorAtIndex(int index) +{ + if (m_matrix == nullptr || m_matrix->getColor(index).isValid() == false) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetColor1 + index, m_matrix->id(), m_matrix->getColor(index), QColor()); + m_matrix->setColor(index, QColor()); + if (index < 2) + m_previewStepHandler->calculateColorDelta(m_matrix->getColor(0), m_matrix->getColor(1), m_matrix->algorithm()); + emit algoColorsChanged(); +} + +bool HUEMatrixEditor::hasColorAtIndex(int index) const +{ + if (m_matrix == nullptr || m_matrix->getColor(index).isValid() == false) + return false; + + return true; +} + +void HUEMatrixEditor::updateColors() +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return; + + int accColors = m_matrix->algorithm()->acceptColors(); + if (accColors == 0) + return; + + if (m_matrix->blendMode() == Universe::MaskBlend || + (m_matrix->controlMode() != RGBMatrix::ControlModeRgb && + m_matrix->controlMode() != RGBMatrix::ControlModeRgbw && + m_matrix->controlMode() != RGBMatrix::ControlModeRgbwBrighter)) + { + m_matrix->setColor(0, Qt::white); + // Overwrite more colors only if applied. + if (accColors <= 2) + m_matrix->setColor(1, QColor()); + if (accColors <= 3) + m_matrix->setColor(2, QColor()); + if (accColors <= 4) + m_matrix->setColor(3, QColor()); + if (accColors <= 5) + m_matrix->setColor(4, QColor()); + } +} + +QString HUEMatrixEditor::algoText() const +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + return algo->text(); + } + + return QString(" Q LIGHT CONTROLLER + "); +} + +void HUEMatrixEditor::setAlgoText(QString text) +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + if (algo->text() == text) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetText, m_matrix->id(), algo->text(), text); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setText(text); + emit algoTextChanged(text); + } +} + +QFont HUEMatrixEditor::algoTextFont() const +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + return algo->font(); + } + return QFont(); +} + +void HUEMatrixEditor::setAlgoTextFont(QFont algoTextFont) +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + if (algo->font() == algoTextFont) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetTextFont, m_matrix->id(), algo->font(), algoTextFont); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setFont(algoTextFont); + emit algoTextFontChanged(algoTextFont); + } +} + +QString HUEMatrixEditor::algoImagePath() const +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + return algo->filename(); + } + + return QString(); +} + +void HUEMatrixEditor::setAlgoImagePath(QString path) +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr && + m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + + if (path.startsWith("file:")) + path = QUrl(path).toLocalFile(); + + if (algo->filename() == path) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetImage, m_matrix->id(), algo->filename(), path); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setFilename(path); + emit algoImagePathChanged(path); + } +} + +QSize HUEMatrixEditor::algoOffset() const +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr) + { + if (m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + return QSize(algo->xOffset(), algo->yOffset()); + } + else if (m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + return QSize(algo->xOffset(), algo->yOffset()); + } + } + return QSize(0, 0); +} + +void HUEMatrixEditor::setAlgoOffset(QSize algoOffset) +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr) + { + if (m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + if (algo->xOffset() == algoOffset.width() && algo->yOffset() == algoOffset.height()) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetOffset, m_matrix->id(), + QSize(algo->xOffset(), algo->yOffset()), algoOffset); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setXOffset(algoOffset.width()); + algo->setYOffset(algoOffset.height()); + emit algoOffsetChanged(algoOffset); + } + else if (m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + if (algo->xOffset() == algoOffset.width() && algo->yOffset() == algoOffset.height()) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetOffset, m_matrix->id(), + QSize(algo->xOffset(), algo->yOffset()), algoOffset); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setXOffset(algoOffset.width()); + algo->setYOffset(algoOffset.height()); + emit algoOffsetChanged(algoOffset); + } + } +} + +int HUEMatrixEditor::animationStyle() const +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr) + { + if (m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + return (int)algo->animationStyle(); + } + else if (m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + return (int)algo->animationStyle(); + } + } + return 0; +} + +void HUEMatrixEditor::setAnimationStyle(int style) +{ + if (m_matrix != nullptr && m_matrix->algorithm() != nullptr) + { + if (m_matrix->algorithm()->type() == RGBAlgorithm::Image) + { + RGBImage *algo = static_cast (m_matrix->algorithm()); + if ((int)algo->animationStyle() == style) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetAnimationStyle, m_matrix->id(), (int)algo->animationStyle(), style); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setAnimationStyle(RGBImage::AnimationStyle(style)); + emit animationStyleChanged(style); + } + else if (m_matrix->algorithm()->type() == RGBAlgorithm::Text) + { + RGBText *algo = static_cast (m_matrix->algorithm()); + if ((int)algo->animationStyle() == style) + return; + + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetAnimationStyle, m_matrix->id(), (int)algo->animationStyle(), style); + QMutexLocker algorithmLocker(&m_matrix->algorithmMutex()); + algo->setAnimationStyle(RGBText::AnimationStyle(style)); + emit animationStyleChanged(style); + } + } +} + +void HUEMatrixEditor::createScriptObjects(QQuickItem *parent) +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr || + m_matrix->algorithm()->type() != RGBAlgorithm::Script) + return; + + RGBScript *script = static_cast (m_matrix->algorithm()); + QList properties = script->properties(); + + foreach (RGBScriptProperty prop, properties) + { + // always create a label first + QMetaObject::invokeMethod(parent, "addLabel", + Q_ARG(QVariant, prop.m_displayName)); + QString pValue = m_matrix->property(prop.m_name); + + switch(prop.m_type) + { + case RGBScriptProperty::List: + { + QVariantList valList; + int idx = 0; + int currIdx = 0; + + foreach (QString val, prop.m_listValues) + { + if (val == pValue) + currIdx = idx; + + QVariantMap valMap; + valMap.insert("mIcon", ""); + valMap.insert("mLabel", val); + valMap.insert("mValue", idx++); + valList.append(valMap); + } + + QMetaObject::invokeMethod(parent, "addComboBox", + Q_ARG(QVariant, prop.m_name), + Q_ARG(QVariant, QVariant::fromValue(valList)), + Q_ARG(QVariant, currIdx)); + } + break; + case RGBScriptProperty::Range: + { + QMetaObject::invokeMethod(parent, "addSpinBox", + Q_ARG(QVariant, prop.m_name), + Q_ARG(QVariant, prop.m_rangeMinValue), + Q_ARG(QVariant, prop.m_rangeMaxValue), + Q_ARG(QVariant, pValue.toInt())); + } + break; + case RGBScriptProperty::Float: + { + QMetaObject::invokeMethod(parent, "addDoubleSpinBox", + Q_ARG(QVariant, prop.m_name), + Q_ARG(QVariant, pValue.toDouble())); + } + break; + case RGBScriptProperty::String: + { + QMetaObject::invokeMethod(parent, "addTextEdit", + Q_ARG(QVariant, prop.m_name), + Q_ARG(QVariant, pValue)); + } + break; + default: + qWarning() << "Type" << prop.m_type << "not handled yet"; + break; + } + } +} + +void HUEMatrixEditor::setScriptStringProperty(QString paramName, QString value) +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr || + m_matrix->algorithm()->type() != RGBAlgorithm::Script) + return; + + qDebug() << "[setScriptStringProperty] param:" << paramName << ", value:" << value; + + StringStringPair oldValue(paramName, m_matrix->property(paramName)); + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetScriptStringValue, m_matrix->id(), QVariant::fromValue(oldValue), + QVariant::fromValue(StringStringPair(paramName, value))); + + m_matrix->setProperty(paramName, value); + emit algoColorsCountChanged(); +} + +void HUEMatrixEditor::setScriptIntProperty(QString paramName, int value) +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr || + m_matrix->algorithm()->type() != RGBAlgorithm::Script) + return; + + qDebug() << "[setScriptIntProperty] param:" << paramName << ", value:" << value; + + StringIntPair oldValue(paramName, m_matrix->property(paramName).toInt()); + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetScriptIntValue, m_matrix->id(), QVariant::fromValue(oldValue), + QVariant::fromValue(StringIntPair(paramName, value))); + m_matrix->setProperty(paramName, QString::number(value)); +} + +void HUEMatrixEditor::setScriptFloatProperty(QString paramName, double value) +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr || + m_matrix->algorithm()->type() != RGBAlgorithm::Script) + return; + + qDebug() << "[setScriptIntProperty] param:" << paramName << ", value:" << value; + + StringDoublePair oldValue(paramName, m_matrix->property(paramName).toDouble()); + Tardis::instance()->enqueueAction(Tardis::RGBMatrixSetScriptDoubleValue, m_matrix->id(), QVariant::fromValue(oldValue), + QVariant::fromValue(StringDoublePair(paramName, value))); + m_matrix->setProperty(paramName, QString::number(value)); +} + +/************************************************************************ + * Blend mode + ************************************************************************/ + +int HUEMatrixEditor::blendMode() const +{ + if (m_matrix == nullptr) + return 0; + + return m_matrix->blendMode(); +} + +void HUEMatrixEditor::setBlendMode(int mode) +{ + if (m_matrix == nullptr || mode == m_matrix->blendMode()) + return; + + m_matrix->setBlendMode(Universe::BlendMode(mode)); + updateColors(); + initPreviewData(); + + emit blendModeChanged(); +} + +/************************************************************************ + * Control mode + ************************************************************************/ + +int HUEMatrixEditor::controlMode() const +{ + if (m_matrix == nullptr) + return 0; + + return m_matrix->controlMode(); +} + +void HUEMatrixEditor::setControlMode(int mode) +{ + if (m_matrix == nullptr || mode == m_matrix->controlMode()) + return; + + m_matrix->setControlMode(RGBMatrix::ControlMode(mode)); + updateColors(); + initPreviewData(); + + emit controlModeChanged(); + emit algoColorsChanged(); +} + +/************************************************************************ + * Rotation & Mirror + ************************************************************************/ + +int HUEMatrixEditor::rotation() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->rotation(); +} + +void HUEMatrixEditor::setRotation(int r) +{ + if (m_matrix == nullptr || r == m_matrix->rotation()) + return; + m_matrix->setRotation(r); + initPreviewData(); + emit rotationChanged(); +} + +int HUEMatrixEditor::mirror() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->mirror(); +} + +void HUEMatrixEditor::setMirror(int m) +{ + if (m_matrix == nullptr || m == m_matrix->mirror()) + return; + m_matrix->setMirror(m); + initPreviewData(); + emit mirrorChanged(); +} + +int HUEMatrixEditor::mirrorBlend() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->mirrorBlend(); +} + +void HUEMatrixEditor::setMirrorBlend(int b) +{ + if (m_matrix == nullptr || b == m_matrix->mirrorBlend()) + return; + m_matrix->setMirrorBlend(HUEMatrix::MirrorBlend(b)); + initPreviewData(); + emit mirrorBlendChanged(); +} + +qreal HUEMatrixEditor::brightness() const +{ + if (m_matrix == nullptr) + return 1.0; + return m_matrix->brightness(); +} + +void HUEMatrixEditor::setBrightness(qreal b) +{ + if (m_matrix == nullptr || qFuzzyCompare(b, m_matrix->brightness())) + return; + m_matrix->setBrightness(b); + emit brightnessChanged(); +} + +/************************************************************************ + * Beat Transform + ************************************************************************/ + +int HUEMatrixEditor::beatEffect() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->beatEffect(); +} + +void HUEMatrixEditor::setBeatEffect(int e) +{ + if (m_matrix == nullptr || e == m_matrix->beatEffect()) + return; + m_matrix->setBeatEffect(HUEMatrix::BeatEffect(e)); + initPreviewData(); + emit beatEffectChanged(); +} + +int HUEMatrixEditor::beatSelection() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->beatSelection(); +} + +void HUEMatrixEditor::setBeatSelection(int s) +{ + if (m_matrix == nullptr || s == m_matrix->beatSelection()) + return; + m_matrix->setBeatSelection(HUEMatrix::BeatSelection(s)); + initPreviewData(); + emit beatSelectionChanged(); +} + +int HUEMatrixEditor::beatOrientation() const +{ + if (m_matrix == nullptr) + return 0; + return m_matrix->beatOrientation(); +} + +void HUEMatrixEditor::setBeatOrientation(int o) +{ + if (m_matrix == nullptr || o == m_matrix->beatOrientation()) + return; + m_matrix->setBeatOrientation(HUEMatrix::BeatOrientation(o)); + initPreviewData(); + emit beatOrientationChanged(); +} + +/************************************************************************ + * Audio Routing + ************************************************************************/ + +bool HUEMatrixEditor::algorithmUsesAudio() const +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return false; + return m_matrix->algorithm()->usesAudio(); +} + +QStringList HUEMatrixEditor::audioInputCategories() const +{ + if (m_matrix == nullptr || m_matrix->algorithm() == nullptr) + return {}; + return m_matrix->algorithm()->audioInputCategories(); +} + +/************************************************************************ + * Save to Sequence + ************************************************************************/ + +void HUEMatrixEditor::saveToSequence() +{ + if (m_matrix == NULL || m_matrix->fixtureGroup() == FixtureGroup::invalidId()) + return; + + FixtureGroup* grp = m_doc->fixtureGroup(m_matrix->fixtureGroup()); + if (grp != NULL && m_matrix->algorithm() != NULL) + { + bool testRunning = m_matrix->isRunning(); + if (testRunning) + m_matrix->stopAndWait(); + + m_previewTimer->stop(); + + Scene *grpScene = new Scene(m_doc); + grpScene->setName(grp->name()); + grpScene->setVisible(false); + + QList headList = grp->headList(); + foreach (GroupHead head, headList) + { + Fixture *fxi = m_doc->fixture(head.fxi); + if (fxi == NULL) + continue; + + if (controlMode() == RGBMatrix::ControlModeRgb) + { + QVector rgb = fxi->rgbChannels(head.head); + + // in case of CMY, dump those channels + if (rgb.count() == 0) + rgb = fxi->cmyChannels(head.head); + + if (rgb.count() == 3) + { + grpScene->setValue(head.fxi, rgb.at(0), 0); + grpScene->setValue(head.fxi, rgb.at(1), 0); + grpScene->setValue(head.fxi, rgb.at(2), 0); + } + } + else if (controlMode() == RGBMatrix::ControlModeDimmer) + { + quint32 channel = fxi->masterIntensityChannel(); + + if (channel == QLCChannel::invalid()) + channel = fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head.head); + + if (channel != QLCChannel::invalid()) + grpScene->setValue(head.fxi, channel, 0); + } + else + { + quint32 channel = QLCChannel::invalid(); + if (controlMode() == RGBMatrix::ControlModeWhite) + channel = fxi->channelNumber(QLCChannel::White, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeAmber) + channel = fxi->channelNumber(QLCChannel::Amber, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeUV) + channel = fxi->channelNumber(QLCChannel::UV, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeShutter) + { + QLCFixtureHead fHead = fxi->head(head.head); + QVector shutters = fHead.shutterChannels(); + if (shutters.count()) + channel = shutters.first(); + } + + if (channel != QLCChannel::invalid()) + grpScene->setValue(head.fxi, channel, 0); + } + } + m_doc->addFunction(grpScene); + + int totalSteps = m_matrix->stepsCount(); + int increment = 1; + int currentStep = 0; + m_previewStepHandler->setStepColor(m_matrix->getColor(0)); + + if (m_matrix->direction() == Function::Backward) + { + currentStep = totalSteps - 1; + increment = -1; + if (m_matrix->getColor(1).isValid()) + m_previewStepHandler->setStepColor(m_matrix->getColor(1)); + } + m_previewStepHandler->calculateColorDelta(m_matrix->getColor(0), m_matrix->getColor(1), + m_matrix->algorithm()); + + if (m_matrix->runOrder() == RGBMatrix::PingPong) + totalSteps = (totalSteps * 2) - 1; + + Sequence *sequence = new Sequence(m_doc); + sequence->setName(QString("%1 %2").arg(m_matrix->name()).arg(tr("Sequence"))); + sequence->setBoundSceneID(grpScene->id()); + sequence->setDurationMode(Chaser::PerStep); + sequence->setDuration(m_matrix->duration()); + + if (m_matrix->fadeInSpeed() != 0) + { + sequence->setFadeInMode(Chaser::PerStep); + sequence->setFadeInSpeed(m_matrix->fadeInSpeed()); + } + if (m_matrix->fadeOutSpeed() != 0) + { + sequence->setFadeOutMode(Chaser::PerStep); + sequence->setFadeOutSpeed(m_matrix->fadeOutSpeed()); + } + + for (int i = 0; i < totalSteps; i++) + { + m_matrix->previewMap(currentStep, m_previewStepHandler); + ChaserStep step; + step.fid = grpScene->id(); + step.hold = m_matrix->duration() - m_matrix->fadeInSpeed(); + step.duration = m_matrix->duration(); + step.fadeIn = m_matrix->fadeInSpeed(); + step.fadeOut = m_matrix->fadeOutSpeed(); + + for (int y = 0; y < m_previewStepHandler->m_map.size(); y++) + { + for (int x = 0; x < m_previewStepHandler->m_map[y].size(); x++) + { + uint col = m_previewStepHandler->m_map[y][x]; + GroupHead head = grp->head(QLCPoint(x, y)); + + Fixture *fxi = m_doc->fixture(head.fxi); + if (fxi == NULL) + continue; + + if (controlMode() == RGBMatrix::ControlModeRgb) + { + QVector rgb = fxi->rgbChannels(head.head); + QVector cmy = fxi->cmyChannels(head.head); + + if (rgb.count() == 3) + { + step.values.append(SceneValue(head.fxi, rgb.at(0), qRed(col))); + step.values.append(SceneValue(head.fxi, rgb.at(1), qGreen(col))); + step.values.append(SceneValue(head.fxi, rgb.at(2), qBlue(col))); + } + + if (cmy.count() == 3) + { + QColor cmyCol(col); + + step.values.append(SceneValue(head.fxi, cmy.at(0), cmyCol.cyan())); + step.values.append(SceneValue(head.fxi, cmy.at(1), cmyCol.magenta())); + step.values.append(SceneValue(head.fxi, cmy.at(2), cmyCol.yellow())); + } + } + else if (controlMode() == RGBMatrix::ControlModeDimmer) + { + quint32 channel = fxi->masterIntensityChannel(); + + if (channel == QLCChannel::invalid()) + channel = fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head.head); + + if (channel != QLCChannel::invalid()) + step.values.append(SceneValue(head.fxi, channel, RGBMatrix::rgbToGrey(col))); + } + else + { + quint32 channel = QLCChannel::invalid(); + if (controlMode() == RGBMatrix::ControlModeWhite) + channel = fxi->channelNumber(QLCChannel::White, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeAmber) + channel = fxi->channelNumber(QLCChannel::Amber, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeUV) + channel = fxi->channelNumber(QLCChannel::UV, QLCChannel::MSB, head.head); + else if (controlMode() == RGBMatrix::ControlModeShutter) + { + QLCFixtureHead fHead = fxi->head(head.head); + QVector shutters = fHead.shutterChannels(); + if (shutters.count()) + channel = shutters.first(); + } + + if (channel != QLCChannel::invalid()) + step.values.append(SceneValue(head.fxi, channel, RGBMatrix::rgbToGrey(col))); + } + } + } + // !! Important !! matrix's heads can be displaced randomly but in a sequence + // we absolutely need ordered values. So do it now ! + std::sort(step.values.begin(), step.values.end()); + + sequence->addStep(step); + currentStep += increment; + if (currentStep == totalSteps) + { + if (m_matrix->runOrder() == RGBMatrix::PingPong) + { + currentStep = totalSteps - 2; + increment = -1; + } + else + { + currentStep = 0; + } + } + m_previewStepHandler->updateStepColor(currentStep, m_matrix->getColor(0), m_matrix->stepsCount()); + } + + m_doc->addFunction(sequence); + + if (testRunning == true) + m_matrix->start(m_doc->masterTimer(), FunctionParent::master()); + + m_previewTimer->start(MasterTimer::tick()); + } +} + +/************************************************************************ + * Preview + ************************************************************************/ + +QSize HUEMatrixEditor::previewSize() const +{ + if (m_matrix == nullptr || m_group == nullptr) + return QSize(0, 0); + + return m_group->size(); +} + +QVariantList HUEMatrixEditor::previewData() const +{ + return m_previewData; +} + +void HUEMatrixEditor::slotPreviewTimeout() +{ + if (m_matrix == nullptr || m_group == nullptr || m_matrix->duration() <= 0) + return; + + if (m_matrix->tempoType() == Function::Time) + { + m_previewElapsed += MasterTimer::tick(); + } + else if (m_matrix->tempoType() == Function::Beats) + { + m_previewElapsed += MasterTimer::tick(); + } + + uint effectiveDuration = m_matrix->duration(); + if (m_matrix->tempoType() == Function::Beats) + { + int beatDur = m_doc->masterTimer()->beatTimeDuration(); + effectiveDuration = Function::beatsToTime(effectiveDuration, beatDur); + if (effectiveDuration == 0) + effectiveDuration = MasterTimer::tick(); + } + + if (m_previewElapsed >= effectiveDuration) + { + QMutexLocker locker(&m_previewMutex); + + m_previewStepHandler->checkNextStep(m_matrix->runOrder(), m_matrix->getColor(0), + m_matrix->getColor(1), m_matrix->stepsCount()); + + m_matrix->previewMap(m_previewStepHandler->currentStepIndex(), m_previewStepHandler); + + //qDebug() << "Step changing. Index:" << m_previewStepHandler->currentStepIndex() << ", map size:" << map.size(); + + m_previewElapsed = 0; +/* + for (int y = 0; y < map.size(); y++) + { + for (int x = 0; x < map[y].size(); x++) + { + QLCPoint pt(x, y); + if (m_group->head(pt).isValid()) + { + if (shape->color() != QColor(map[y][x]).rgb()) + shape->setColor(map[y][x]); + + if (shape->color() == QColor(Qt::black).rgb()) + shape->draw(m_matrix->fadeOutSpeed()); + else + shape->draw(m_matrix->fadeInSpeed()); + } + } + } +*/ + if (m_previewData.isEmpty() || m_previewStepHandler->m_map.isEmpty()) + return; + + QMapIterator it(m_group->headsMap()); + while (it.hasNext()) + { + it.next(); + + QLCPoint pt(it.key()); + //GroupHead head(it.value()); + int ptIdx = pt.x() + (pt.y() * m_group->size().width()); + if (ptIdx < m_previewData.size()) + m_previewData[ptIdx] = QVariant(QColor(m_previewStepHandler->m_map[pt.y()][pt.x()])); + } + + //qDebug() << "Preview data changed!"; + emit previewDataChanged(m_previewData); + } +} + +void HUEMatrixEditor::slotBeatReceived() +{ + m_gotBeat = true; +} + +void HUEMatrixEditor::initPreviewData() +{ + m_previewTimer->stop(); + m_previewElapsed = 0; + m_previewData.clear(); + QSize pSize = previewSize(); + + for (int i = 0; i < pSize.width() * pSize.height(); i++) + m_previewData.append(QVariant(QColor(0, 0, 0, 0))); + + if (m_matrix == nullptr) + return; + + m_previewStepHandler->initializeDirection(m_matrix->direction(), m_matrix->getColor(0), + m_matrix->getColor(1), m_matrix->stepsCount(), m_matrix->algorithm()); + m_previewStepHandler->calculateColorDelta(m_matrix->getColor(0), m_matrix->getColor(1), m_matrix->algorithm()); + + m_previewTimer->start(MasterTimer::tick()); +} diff --git a/qmlui/huematrixeditor.h b/qmlui/huematrixeditor.h new file mode 100644 index 0000000000..71b491a776 --- /dev/null +++ b/qmlui/huematrixeditor.h @@ -0,0 +1,254 @@ +/* + Q Light Controller Plus + huematrixeditor.h + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef HUEMATRIXEDITOR_H +#define HUEMATRIXEDITOR_H + +#include "functioneditor.h" + +class Doc; +class HUEMatrix; +class RGBMatrixStep; +class FixtureGroup; + +class HUEMatrixEditor final : public FunctionEditor +{ + Q_OBJECT + + Q_PROPERTY(int fixtureGroup READ fixtureGroup WRITE setFixtureGroup NOTIFY fixtureGroupChanged) + + Q_PROPERTY(QSize previewSize READ previewSize NOTIFY previewSizeChanged) + Q_PROPERTY(QVariantList previewData READ previewData NOTIFY previewDataChanged) + + Q_PROPERTY(QStringList algorithms READ algorithms CONSTANT) + Q_PROPERTY(int algorithmIndex READ algorithmIndex WRITE setAlgorithmIndex NOTIFY algorithmIndexChanged) + Q_PROPERTY(int algoColorsCount READ algoColorsCount NOTIFY algoColorsCountChanged) + Q_PROPERTY(QVariantList algoColors READ algoColors NOTIFY algoColorsChanged) + + Q_PROPERTY(int blendMode READ blendMode WRITE setBlendMode NOTIFY blendModeChanged) + Q_PROPERTY(int controlMode READ controlMode WRITE setControlMode NOTIFY controlModeChanged) + + Q_PROPERTY(int rotation READ rotation WRITE setRotation NOTIFY rotationChanged) + Q_PROPERTY(int mirror READ mirror WRITE setMirror NOTIFY mirrorChanged) + Q_PROPERTY(int mirrorBlend READ mirrorBlend WRITE setMirrorBlend NOTIFY mirrorBlendChanged) + Q_PROPERTY(qreal brightness READ brightness WRITE setBrightness NOTIFY brightnessChanged) + + Q_PROPERTY(int beatEffect READ beatEffect WRITE setBeatEffect NOTIFY beatEffectChanged) + Q_PROPERTY(int beatSelection READ beatSelection WRITE setBeatSelection NOTIFY beatSelectionChanged) + Q_PROPERTY(int beatOrientation READ beatOrientation WRITE setBeatOrientation NOTIFY beatOrientationChanged) + + Q_PROPERTY(bool algorithmUsesAudio READ algorithmUsesAudio NOTIFY algorithmIndexChanged) + Q_PROPERTY(QStringList audioInputCategories READ audioInputCategories NOTIFY algorithmIndexChanged) + + // Text Algorithm specific properties + Q_PROPERTY(QString algoText READ algoText WRITE setAlgoText NOTIFY algoTextChanged) + Q_PROPERTY(QFont algoTextFont READ algoTextFont WRITE setAlgoTextFont NOTIFY algoTextFontChanged) + // Image Algorithm specific properties + Q_PROPERTY(QString algoImagePath READ algoImagePath WRITE setAlgoImagePath NOTIFY algoImagePathChanged) + + Q_PROPERTY(int animationStyle READ animationStyle WRITE setAnimationStyle NOTIFY animationStyleChanged) + Q_PROPERTY(QSize algoOffset READ algoOffset WRITE setAlgoOffset NOTIFY algoOffsetChanged) + +public: + HUEMatrixEditor(QQuickView *view, Doc *doc, QObject *parent = 0); + ~HUEMatrixEditor(); + + /** Set the ID of the RGBMatrix to edit */ + void setFunctionID(quint32 id) override; + + int fixtureGroup() const; + void setFixtureGroup(int fixtureGroup); + + Q_INVOKABLE void saveToSequence(); + +signals: + void fixtureGroupChanged(int fixtureGroup); + +private: + /** Reference of the RGBMatrix currently being edited */ + HUEMatrix *m_matrix; + /** Reference to the Fixture Group associated to the RGBMatrix */ + FixtureGroup *m_group; + + /************************************************************************ + * Algorithm + ************************************************************************/ +public: + QStringList algorithms() const; + + /** Get/set the RGBMatrix algorithm index in the algorithms list */ + int algorithmIndex() const; + void setAlgorithmIndex(int algoIndex); + + /** Return the accepted colors of the current algorithm */ + int algoColorsCount() const; + + QVariantList algoColors(); + + Q_INVOKABLE QColor colorAtIndex(int index) const; + Q_INVOKABLE void setColorAtIndex(int index, QColor color); + Q_INVOKABLE void resetColorAtIndex(int index); + Q_INVOKABLE bool hasColorAtIndex(int index) const; + + QString algoText() const; + void setAlgoText(QString text); + + QFont algoTextFont() const; + void setAlgoTextFont(QFont algoTextFont); + + QString algoImagePath() const; + void setAlgoImagePath(QString path); + + QSize algoOffset() const; + void setAlgoOffset(QSize algoOffset); + + int animationStyle() const; + void setAnimationStyle(int style); + + /** This is an important method called by the QML world + * when a RGBScript algorithm is selected. + * The steps are: + * - QML creates an empty GridLayout. On completed, it invokes this C++ method + * - each parameter label is created by the C++ code + * - combo boxes are created in QML, and C++ sends the model and properties + * - spin boxes are created in QML, and C++ sends the range and properties + */ + Q_INVOKABLE void createScriptObjects(QQuickItem *parent); + + Q_INVOKABLE void setScriptStringProperty(QString paramName, QString value); + Q_INVOKABLE void setScriptIntProperty(QString paramName, int value); + Q_INVOKABLE void setScriptFloatProperty(QString paramName, double value); + +private: + void updateColors(); + +signals: + void algorithmIndexChanged(); + void algoColorsCountChanged(); + void algoColorsChanged(); + + void algoTextChanged(QString text); + void algoTextFontChanged(QFont algoTextFont); + + void algoImagePathChanged(QString path); + + void algoOffsetChanged(QSize algoOffset); + void animationStyleChanged(int style); + +private: + QVariantList m_algoColors; + + /************************************************************************ + * Blend mode + ************************************************************************/ +public: + int blendMode() const; + void setBlendMode(int mode); + +signals: + void blendModeChanged(); + + /************************************************************************ + * Control mode + ************************************************************************/ +public: + int controlMode() const; + void setControlMode(int mode); + +signals: + void controlModeChanged(); + + /************************************************************************ + * Rotation & Mirror + ************************************************************************/ +public: + int rotation() const; + void setRotation(int r); + + int mirror() const; + void setMirror(int m); + + int mirrorBlend() const; + void setMirrorBlend(int b); + + qreal brightness() const; + void setBrightness(qreal b); + +signals: + void rotationChanged(); + void mirrorChanged(); + void mirrorBlendChanged(); + void brightnessChanged(); + + /************************************************************************ + * Beat Transform + ************************************************************************/ +public: + int beatEffect() const; + void setBeatEffect(int e); + + int beatSelection() const; + void setBeatSelection(int s); + + int beatOrientation() const; + void setBeatOrientation(int o); + +signals: + void beatEffectChanged(); + void beatSelectionChanged(); + void beatOrientationChanged(); + + /************************************************************************ + * Audio Routing + ************************************************************************/ +public: + bool algorithmUsesAudio() const; + QStringList audioInputCategories() const; + + /************************************************************************ + * Preview + ************************************************************************/ +public: + QSize previewSize() const; + QVariantList previewData() const; + +private slots: + void slotPreviewTimeout(); + void slotBeatReceived(); + +private: + void initPreviewData(); + +signals: + void previewSizeChanged(); + void previewDataChanged(QVariantList matrixData); + +private: + /** A timer to perform a timed preview of the RGBMatrix pattern */ + QTimer* m_previewTimer; + uint m_previewElapsed; + RGBMatrixStep *m_previewStepHandler; + bool m_gotBeat; + QMutex m_previewMutex; + + /** exchange variable with the QML world */ + QVariantList m_previewData; +}; + +#endif // HUEMATRIXEDITOR_H diff --git a/qmlui/importmanager.cpp b/qmlui/importmanager.cpp index 98acf6d4f6..49e6514444 100644 --- a/qmlui/importmanager.cpp +++ b/qmlui/importmanager.cpp @@ -27,6 +27,7 @@ #include "qlcfixturedefcache.h" #include "monitorproperties.h" #include "rgbscriptscache.h" +#include "huescriptscache.h" #include "qlcfixturemode.h" #include "qlcfixturedef.h" #include "fixtureutils.h" @@ -67,6 +68,12 @@ ImportManager::ImportManager(QQuickView *view, Doc *doc, QObject *parent) m_importDoc->rgbScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory()); m_importDoc->rgbScriptsCache()->load(RGBScriptsCache::userScriptsDirectory()); + /* Load HUE scripts so imported HUEMatrix functions resolve their algorithm */ + m_importDoc->hueScriptsCache()->load(HUEScriptsCache::systemScriptsDirectory(), true); + m_importDoc->hueScriptsCache()->load(HUEScriptsCache::userScriptsDirectory(), true); + m_importDoc->hueScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory(), false); + m_importDoc->hueScriptsCache()->load(RGBScriptsCache::userScriptsDirectory(), false); + m_view->rootContext()->setContextProperty("importManager", this); } @@ -580,6 +587,7 @@ void ImportManager::importFunctionID(quint32 funcID) } break; case Function::RGBMatrixType: + case Function::HUEMatrixType: { RGBMatrix *rgbm = qobject_cast(docFunction); if (rgbm->fixtureGroup() == FixtureGroup::invalidId()) @@ -958,6 +966,7 @@ void ImportManager::checkFunctionDependency(quint32 fid) // RGB Matrix requires a fixture group case Function::RGBMatrixType: + case Function::HUEMatrixType: { RGBMatrix *rgbm = qobject_cast(func); fxList = rgbm->components(); diff --git a/qmlui/qml/fixturesfunctions/AddFunctionMenu.qml b/qmlui/qml/fixturesfunctions/AddFunctionMenu.qml index 70dda975db..17c2e33c8a 100644 --- a/qmlui/qml/fixturesfunctions/AddFunctionMenu.qml +++ b/qmlui/qml/fixturesfunctions/AddFunctionMenu.qml @@ -87,6 +87,12 @@ Popup onClicked: entryClicked(QLCFunction.RGBMatrixType) } ContextMenuEntry + { + imgSource: "qrc:/huematrix.svg" + entryText: qsTr("New HUE Matrix") + onClicked: entryClicked(QLCFunction.HUEMatrixType) + } + ContextMenuEntry { imgSource: "qrc:/showmanager.svg" entryText: qsTr("New Show") diff --git a/qmlui/qml/fixturesfunctions/FunctionManager.qml b/qmlui/qml/fixturesfunctions/FunctionManager.qml index fd9759503b..f1f8b294c9 100644 --- a/qmlui/qml/fixturesfunctions/FunctionManager.qml +++ b/qmlui/qml/fixturesfunctions/FunctionManager.qml @@ -161,6 +161,18 @@ Rectangle onCheckedChanged: setFunctionFilter(QLCFunction.RGBMatrixType, checked) } IconButton + { + z: 2 + width: height + height: topBar.height - 2 + imgSource: "qrc:/huematrix.svg" + checkable: true + checked: functionManager.functionsFilter & QLCFunction.HUEMatrixType + tooltip: qsTr("HUE Matrices") + counter: functionManager.hueMatrixCount + onCheckedChanged: setFunctionFilter(QLCFunction.HUEMatrixType, checked) + } + IconButton { z: 2 width: height diff --git a/qmlui/qml/fixturesfunctions/HUEMatrixEditor.qml b/qmlui/qml/fixturesfunctions/HUEMatrixEditor.qml new file mode 100644 index 0000000000..06ca6e62a1 --- /dev/null +++ b/qmlui/qml/fixturesfunctions/HUEMatrixEditor.qml @@ -0,0 +1,1221 @@ +/* + Q Light Controller Plus + HUEMatrixEditor.qml + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Dialogs + +import org.qlcplus.classes 1.0 + +import "TimeUtils.js" as TimeUtils +import "." + +Rectangle +{ + id: rgbmeContainer + //anchors.fill: parent + color: "transparent" + + property int functionID: -1 + property var algoColors: hueMatrixEditor ? hueMatrixEditor.algoColors : null + + signal requestView(int ID, string qmlSrc, bool back) + + + TimeEditTool + { + id: timeEditTool + + parent: mainView + z: 99 + x: rightSidePanel.x - width + visible: false + tempoType: hueMatrixEditor.tempoType + + onValueChanged: + { + if (speedType == QLCFunction.FadeIn) + hueMatrixEditor.fadeInSpeed = val + else if (speedType == QLCFunction.Hold) + hueMatrixEditor.holdSpeed = val + else if (speedType == QLCFunction.FadeOut) + hueMatrixEditor.fadeOutSpeed = val + } + } + + ColorTool + { + id: colorTool + x: -width - (UISettings.iconSizeDefault * 1.25) + y: UISettings.bigItemHeight + visible: false + closeOnSelect: true + + property int colorIndex: -1 + property Item previewBtn + + function showTool(index, button) + { + colorIndex = index + previewBtn = button + currentRGB = hueMatrixEditor.colorAtIndex(colorIndex) + visible = true + } + + function hide() + { + visible = false + colorIndex = -1 + previewBtn = null + } + + onToolColorChanged: + function(r, g, b, w, a, uv) + { + hueMatrixEditor.setColorAtIndex(colorIndex, Qt.rgba(r, g, b, 1.0)) + } + onClose: visible = false + } + + EditorTopBar + { + id: topBar + text: hueMatrixEditor.functionName + onTextChanged: hueMatrixEditor.functionName = text + + onBackClicked: + { + var prevID = hueMatrixEditor.previousID + requestView(prevID, functionManager.getEditorResource(prevID), true) + } + } + + Flickable + { + id: editorFlickable + x: 5 + y: topBar.height + 2 + width: parent.width - 10 + height: parent.height - y + + contentHeight: editorColumn.height + boundsBehavior: Flickable.StopAtBounds + + Component.onCompleted: console.log("Flickable height: " + height + ", Grid height: " + editorColumn.height + ", parent height: " + parent.height) + + Column + { + id: editorColumn + width: parent.width + spacing: 2 + + property int itemsHeight: UISettings.listItemHeight + property int firstColumnWidth: 0 + property int colWidth: parent.width - (sbar.visible ? sbar.width : 0) + + //onHeightChanged: editorFlickable.contentHeight = height //console.log("Grid layout height changed: " + height) + + function checkLabelWidth(w) + { + firstColumnWidth = Math.max(w, firstColumnWidth) + } + + // row 1 + RowLayout + { + width: editorColumn.colWidth + + RobotoText + { + label: qsTr("Fixture Group") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: fixtureGroupEditor.groupsListModel + currValue: hueMatrixEditor.fixtureGroup + onValueChanged: (value) => hueMatrixEditor.fixtureGroup = value + } + } + + // row 2 + RGBMatrixPreview + { + width: editorColumn.width + matrixSize: hueMatrixEditor.previewSize + matrixData: hueMatrixEditor.previewData + maximumHeight: rgbmeContainer.height / 3 + } + + + GridLayout + { + width: editorColumn.colWidth + columns: 3 + + // row 3 + RobotoText + { + id: patternLabel + label: qsTr("Pattern") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + id: algoCombo + Layout.fillWidth: true + height: editorColumn.itemsHeight + textRole: "" + model: hueMatrixEditor.algorithms + currentIndex: hueMatrixEditor.algorithmIndex + onDisplayTextChanged: + { + hueMatrixEditor.algorithmIndex = currentIndex + paramSection.sectionContents = null + if (displayText === "Text") + paramSection.sectionContents = textAlgoComponent + else if (displayText === "Image") + paramSection.sectionContents = imageAlgoComponent + else + paramSection.sectionContents = scriptAlgoComponent + } + } + + Row + { + height: editorColumn.itemsHeight + spacing: 4 + + IconButton + { + width: UISettings.listItemHeight + height: width + imgSource: "qrc:/sequence.svg" + tooltip: qsTr("Save this matrix to a sequence") + onClicked: hueMatrixEditor.saveToSequence() + } + } + + // row 4 + RobotoText + { + label: qsTr("Blend mode") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Default (HTP)") }, + { mLabel: qsTr("Mask") }, + { mLabel: qsTr("Additive") }, + { mLabel: qsTr("Subtractive") } + ] + + currentIndex: hueMatrixEditor.blendMode + onCurrentIndexChanged: hueMatrixEditor.blendMode = currentIndex + } + + // row 5 + RobotoText + { + label: qsTr("Color mode") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Default (RGB)") }, + { mLabel: qsTr("White") }, + { mLabel: qsTr("Amber") }, + { mLabel: qsTr("UV") }, + { mLabel: qsTr("Dimmer") }, + { mLabel: qsTr("Shutter") }, + { mLabel: qsTr("RGBW (Accurate)") }, + { mLabel: qsTr("RGBW (Brighter)") } + ] + + currentIndex: hueMatrixEditor.controlMode + onCurrentIndexChanged: hueMatrixEditor.controlMode = currentIndex + } + + // Brightness + RobotoText + { + label: qsTr("Brightness") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomDoubleSpinBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + decimals: 2 + realFrom: 0 + realTo: 1000 + realStep: 0.01 + realValue: hueMatrixEditor.brightness + onRealValueChanged: hueMatrixEditor.brightness = realValue + } + + // Rotation + RobotoText + { + label: qsTr("Rotation") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("None") }, + { mLabel: qsTr("90\u00B0 CW") }, + { mLabel: qsTr("180\u00B0") }, + { mLabel: qsTr("270\u00B0 CW") } + ] + currentIndex: hueMatrixEditor.rotation + onCurrentIndexChanged: hueMatrixEditor.rotation = currentIndex + } + + // Mirror + RobotoText + { + label: qsTr("Mirror") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Off") }, + { mLabel: qsTr("Horizontal") }, + { mLabel: qsTr("Vertical") }, + { mLabel: qsTr("Both") } + ] + currentIndex: hueMatrixEditor.mirror + onCurrentIndexChanged: hueMatrixEditor.mirror = currentIndex + } + + // Mirror Blend (only visible when mirror is active) + RobotoText + { + visible: hueMatrixEditor.mirror > 0 + label: qsTr("Mirror blend") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + visible: hueMatrixEditor.mirror > 0 + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Flip") }, + { mLabel: qsTr("Max") }, + { mLabel: qsTr("Average") }, + { mLabel: qsTr("Additive") } + ] + currentIndex: hueMatrixEditor.mirrorBlend + onCurrentIndexChanged: hueMatrixEditor.mirrorBlend = currentIndex + } + + // Beat Transform - Effect + RobotoText + { + label: qsTr("Beat effect") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Off") }, + { mLabel: qsTr("Mirror") }, + { mLabel: qsTr("Color Invert") }, + { mLabel: qsTr("Blackout") }, + { mLabel: qsTr("Whiteout") } + ] + currentIndex: hueMatrixEditor.beatEffect + onCurrentIndexChanged: hueMatrixEditor.beatEffect = currentIndex + } + + // Beat Transform - Selection (only visible when effect != Off) + RobotoText + { + visible: hueMatrixEditor.beatEffect > 0 + label: qsTr("Beat selection") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + visible: hueMatrixEditor.beatEffect > 0 + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("All on downbeat") }, + { mLabel: qsTr("Walk") }, + { mLabel: qsTr("Random") } + ] + currentIndex: hueMatrixEditor.beatSelection + onCurrentIndexChanged: hueMatrixEditor.beatSelection = currentIndex + } + + // Beat Transform - Orientation (only visible when effect != Off) + RobotoText + { + visible: hueMatrixEditor.beatEffect > 0 + label: qsTr("Beat orientation") + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + CustomComboBox + { + visible: hueMatrixEditor.beatEffect > 0 + Layout.columnSpan: 2 + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Rows") }, + { mLabel: qsTr("Columns") } + ] + currentIndex: hueMatrixEditor.beatOrientation + onCurrentIndexChanged: hueMatrixEditor.beatOrientation = currentIndex + } + + + // row 6: dynamic color pickers (one per algoColorsCount) + RobotoText + { + id: colorLabel + label: qsTr("Colors") + visible: hueMatrixEditor.algoColorsCount > 0 + height: editorColumn.itemsHeight + onWidthChanged: + { + editorColumn.checkLabelWidth(width) + width = Qt.binding(function() { return editorColumn.firstColumnWidth }) + } + } + + Flow + { + Layout.columnSpan: 2 + Layout.fillWidth: true + spacing: 6 + visible: hueMatrixEditor.algoColorsCount > 0 + + Repeater + { + model: Math.min(hueMatrixEditor.algoColorsCount, 5) + + delegate: Row + { + spacing: 2 + + Rectangle + { + id: colorBtn + width: UISettings.iconSizeDefault * 2 + height: editorColumn.itemsHeight + radius: 5 + border.color: colorMA.containsMouse ? "white" : UISettings.bgLight + border.width: 2 + color: rgbmeContainer.algoColors && hueMatrixEditor.hasColorAtIndex(index) + ? rgbmeContainer.algoColors[index] : "transparent" + + MouseArea + { + id: colorMA + anchors.fill: parent + hoverEnabled: true + onClicked: + { + if (colorTool.visible) + colorTool.hide() + else + colorTool.showTool(index, colorBtn) + } + } + } + IconButton + { + width: UISettings.listItemHeight + height: width + faSource: FontAwesome.fa_xmark + faColor: "darkred" + tooltip: qsTr("Reset color %1").arg(index + 1) + visible: index > 0 + onClicked: hueMatrixEditor.resetColorAtIndex(index) + } + } + } + } + } + + SectionBox + { + id: paramSection + width: editorColumn.colWidth - 5 + visible: sectionContents ? true : false + + sectionLabel: qsTr("Parameters") + sectionContents: null + } + + SectionBox + { + id: speedSection + width: editorColumn.colWidth - 5 + isExpanded: false + sectionLabel: qsTr("Speed") + sectionContents: + GridLayout + { + width: parent.width + columns: 3 + columnSpacing: 5 + rowSpacing: 4 + + function showTimeTool(item, titleLabel, timeLabel, type) + { + timeEditTool.allowFractions = QLCFunction.FineFractions + timeEditTool.show(-1, item.mapToItem(mainView, 0, 0).y - timeEditTool.height, + titleLabel, timeLabel, type) + } + + // Row 1 + RobotoText + { + id: fiLabel + label: qsTr("Steps fade in") + height: UISettings.listItemHeight + } + + Rectangle + { + Layout.fillWidth: true + height: UISettings.listItemHeight + color: UISettings.bgMedium + + RobotoText + { + id: fiTimeLabel + x: 3 + height: parent.height + label: TimeUtils.timeToQlcString(hueMatrixEditor.fadeInSpeed, hueMatrixEditor.tempoType) + } + MouseArea + { + anchors.fill: parent + onDoubleClicked: showTimeTool(this, fiLabel.label, fiTimeLabel.label, QLCFunction.FadeIn) + } + } + + IconButton + { + width: height + height: UISettings.listItemHeight + faSource: FontAwesome.fa_clock + faColor: UISettings.fgMain + onClicked: showTimeTool(this, fiLabel.label, fiTimeLabel.label, QLCFunction.FadeIn) + } + + // Row 2 + RobotoText + { + id: hLabel + height: UISettings.listItemHeight + label: qsTr("Steps hold") + } + + Rectangle + { + Layout.fillWidth: true + height: UISettings.listItemHeight + color: UISettings.bgMedium + + RobotoText + { + id: hTimeLabel + x: 3 + height: parent.height + label: TimeUtils.timeToQlcString(hueMatrixEditor.holdSpeed, hueMatrixEditor.tempoType) + } + MouseArea + { + anchors.fill: parent + onDoubleClicked: showTimeTool(this, hLabel.label, hTimeLabel.label, QLCFunction.Hold) + } + } + + IconButton + { + width: height + height: UISettings.listItemHeight + faSource: FontAwesome.fa_clock + faColor: UISettings.fgMain + onClicked: showTimeTool(this, hLabel.label, hTimeLabel.label, QLCFunction.Hold) + } + + // Row 3 + RobotoText + { + id: foLabel + height: UISettings.listItemHeight + label: qsTr("Steps fade out") + } + + Rectangle + { + Layout.fillWidth: true + height: UISettings.listItemHeight + color: UISettings.bgMedium + + RobotoText + { + id: foTimeLabel + x: 3 + height: parent.height + label: TimeUtils.timeToQlcString(hueMatrixEditor.fadeOutSpeed, hueMatrixEditor.tempoType) + } + MouseArea + { + anchors.fill: parent + onDoubleClicked: showTimeTool(this, foLabel.label, foTimeLabel.label, QLCFunction.FadeOut) + } + } + + IconButton + { + width: height + height: UISettings.listItemHeight + faSource: FontAwesome.fa_clock + faColor: UISettings.fgMain + onClicked: showTimeTool(this, foLabel.label, foTimeLabel.label, QLCFunction.FadeOut) + } + + // Row 4 + RobotoText + { + id: ttLabel + height: UISettings.listItemHeight + label: qsTr("Tempo type") + } + CustomComboBox + { + Layout.columnSpan: 2 + Layout.fillWidth: true + height: UISettings.listItemHeight + model: [ + { mLabel: qsTr("Time"), mValue: QLCFunction.Time }, + { mLabel: qsTr("Beats"), mValue: QLCFunction.Beats } + ] + + currValue: hueMatrixEditor.tempoType + onValueChanged: hueMatrixEditor.tempoType = value + } + Item + { + width: UISettings.listItemHeight + height: width + } + + // Row 5 + RobotoText + { + label: qsTr("Step duration") + height: UISettings.listItemHeight + } + Rectangle + { + Layout.fillWidth: true + height: UISettings.listItemHeight + color: UISettings.bgMedium + + RobotoText + { + x: 3 + height: parent.height + label: { + var dur = hueMatrixEditor.duration + if (dur === -2) return "∞" + return TimeUtils.timeToQlcString(dur, hueMatrixEditor.tempoType) + } + } + } + Item + { + width: UISettings.listItemHeight + height: width + } + } + } + + SectionBox + { + id: directionSection + width: editorColumn.colWidth - 5 + sectionLabel: qsTr("Order and direction") + sectionContents: + GridLayout + { + width: parent.width + columns: 4 + columnSpacing: 4 + rowSpacing: 4 + + // Row 1 + IconPopupButton + { + model: [ + { mLabel: qsTr("Loop"), faIcon: FontAwesome.fa_retweet, mValue: QLCFunction.Loop }, + { mLabel: qsTr("Single Shot"), faIcon: FontAwesome.fa_right_long, mValue: QLCFunction.SingleShot }, + { mLabel: qsTr("Ping Pong"), faIcon: FontAwesome.fa_right_left, mValue: QLCFunction.PingPong } + ] + + currValue: hueMatrixEditor.runOrder + onValueChanged: hueMatrixEditor.runOrder = value + } + RobotoText + { + label: qsTr("Run Order") + Layout.fillWidth: true + } + + IconPopupButton + { + model: [ + { mLabel: qsTr("Forward"), faIcon: FontAwesome.fa_angles_right, mValue: QLCFunction.Forward }, + { mLabel: qsTr("Backward"), faIcon: FontAwesome.fa_angles_left, mValue: QLCFunction.Backward } + ] + + currValue: hueMatrixEditor.direction + onValueChanged: hueMatrixEditor.direction = value + } + RobotoText + { + label: qsTr("Direction") + Layout.fillWidth: true + } + + } // GridLayout + } + } // Column + ScrollBar.vertical: CustomScrollBar { id: sbar } + } // Flickable + + /* ************************************************************* + * Here starts all the Algorithm-specific Component definitions, + * loaded at runtime depending on the selected algorithm + * *********************************************************** */ + + /* ************************************************************* + * **************** Text Algorithm parameters **************** */ + Component + { + id: textAlgoComponent + GridLayout + { + columns: 2 + columnSpacing: 5 + + // Row 1 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Text") + } + + Rectangle + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + color: "transparent" + + Rectangle + { + height: parent.height + width: parent.width - fontButton.width - 5 + radius: 3 + color: UISettings.bgMedium + border.color: "#222" + + TextInput + { + id: algoTextEdit + anchors.fill: parent + anchors.margins: 4 + anchors.verticalCenter: parent.verticalCenter + text: hueMatrixEditor.algoText + font.family: hueMatrixEditor.algoTextFont.font.family + font.bold: hueMatrixEditor.algoTextFont.font.bold + font.italic: hueMatrixEditor.algoTextFont.font.italic + font.pixelSize: UISettings.textSizeDefault * 0.8 + color: "white" + + onTextEdited: hueMatrixEditor.algoText = text + } + } + IconButton + { + id: fontButton + width: UISettings.iconSizeMedium + height: width + anchors.right: parent.right + faSource: FontAwesome.fa_font + faColor: "lightcyan" + + onClicked: fontDialog.visible = true + + FontDialog + { + id: fontDialog + title: qsTr("Please choose a font") + selectedFont: hueMatrixEditor.algoTextFont + visible: false + + onAccepted: + { + console.log("Selected font: " + selectedFont) + hueMatrixEditor.algoTextFont = selectedFont + } + } + } + } + + // Row 2 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Animation") + } + CustomComboBox + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Letters") }, + { mLabel: qsTr("Horizontal") }, + { mLabel: qsTr("Vertical") } + ] + + currentIndex: hueMatrixEditor.animationStyle + onCurrentIndexChanged: hueMatrixEditor.animationStyle = currentIndex + } + + // Row 3 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Offset") + } + Rectangle + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + color: "transparent" + + Row + { + id: toffRow + spacing: 20 + anchors.fill: parent + + property size algoOffset: hueMatrixEditor.algoOffset + + RobotoText { height: UISettings.listItemHeight; label: qsTr("X") } + CustomSpinBox + { + height: parent.height + from: -255 + to: 255 + value: toffRow.algoOffset.width + onValueModified: + { + var newOffset = toffRow.algoOffset + newOffset.width = value + hueMatrixEditor.algoOffset = newOffset + } + } + + RobotoText { height: UISettings.listItemHeight; label: qsTr("Y") } + CustomSpinBox + { + height: parent.height + from: -255 + to: 255 + value: toffRow.algoOffset.height + onValueModified: + { + var newOffset = toffRow.algoOffset + newOffset.height = value + hueMatrixEditor.algoOffset = newOffset + } + } + } + } + } + } + + /* ************************************************************* + * **************** Image Algorithm parameters *************** */ + Component + { + id: imageAlgoComponent + + GridLayout + { + id: imageAlgoGrid + columns: 2 + columnSpacing: 5 + + // Row 1 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Image") + } + Rectangle + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + color: "transparent" + + Rectangle + { + height: parent.height + width: parent.width - imgButton.width - 5 + radius: 3 + color: UISettings.bgMedium + border.color: UISettings.bgStrong + clip: true + + TextInput + { + id: algoTextEdit + anchors.fill: parent + anchors.margins: 4 + anchors.verticalCenter: parent.verticalCenter + text: hueMatrixEditor.algoImagePath + font.pixelSize: UISettings.textSizeDefault + color: "white" + + onTextEdited: hueMatrixEditor.algoImagePath = text + } + } + IconButton + { + id: imgButton + width: UISettings.iconSizeMedium + height: width + anchors.right: parent.right + faSource: FontAwesome.fa_image + faColor: "lightyellow" + tooltip: qsTr("Set a custom background") + + onClicked: fileDialog.visible = true + + FileDialog + { + id: fileDialog + visible: false + title: qsTr("Select an image") + nameFilters: [ "Image files (*.png *.bmp *.jpg *.jpeg *.gif)", "All files (*)" ] + + onAccepted: hueMatrixEditor.algoImagePath = fileDialog.selectedFile + } + } + } + + // Row 2 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Animation") + } + CustomComboBox + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + model: [ + { mLabel: qsTr("Static") }, + { mLabel: qsTr("Horizontal") }, + { mLabel: qsTr("Vertical") }, + { mLabel: qsTr("Animation") } + ] + + currentIndex: hueMatrixEditor.animationStyle + onCurrentIndexChanged: hueMatrixEditor.animationStyle = currentIndex + } + + // Row 3 + RobotoText + { + height: UISettings.listItemHeight + label: qsTr("Offset") + } + Rectangle + { + Layout.fillWidth: true + height: editorColumn.itemsHeight + color: "transparent" + + Row + { + id: ioffRow + spacing: 20 + anchors.fill: parent + + property size algoOffset: hueMatrixEditor.algoOffset + + RobotoText { height: UISettings.listItemHeight; label: qsTr("X") } + CustomSpinBox + { + height: parent.height + from: -255 + to: 255 + value: ioffRow.algoOffset.width + onValueModified: + { + var newOffset = ioffRow.algoOffset + newOffset.width = value + hueMatrixEditor.algoOffset = newOffset + } + } + + RobotoText { height: UISettings.listItemHeight; label: qsTr("Y") } + CustomSpinBox + { + height: parent.height + from: -255 + to: 255 + value: ioffRow.algoOffset.height + onValueModified: + { + var newOffset = ioffRow.algoOffset + newOffset.height = value + hueMatrixEditor.algoOffset = newOffset + } + } + } + } + } + } + + /* ************************************************************ */ + /* *************** Script Algorithm parameters *************** */ + Component + { + id: scriptAlgoComponent + + GridLayout + { + id: scriptAlgoGrid + columns: 2 + columnSpacing: 5 + + function addLabel(text) + { + labelComponent.createObject(scriptAlgoGrid, + {"propName": text }); + if (labelComponent.status !== Component.Ready) + console.log("Label component is not ready !!") + } + + function addComboBox(propName, model, currentIndex) + { + comboComponent.createObject(scriptAlgoGrid, + {"propName": propName, "model": model, "currentIndex": currentIndex }); + if (comboComponent.status !== Component.Ready) + console.log("Combo component is not ready !!") + } + + function addSpinBox(propName, min, max, currentValue) + { + spinComponent.createObject(scriptAlgoGrid, + {"propName": propName, "from": min, "to": max, "value": currentValue }); + if (spinComponent.status !== Component.Ready) + console.log("Spin component is not ready !!") + } + + function addDoubleSpinBox(propName, currentValue) + { + doubleSpinComponent.createObject(scriptAlgoGrid, + {"propName": propName, "realValue": currentValue }); + if (spinComponent.status !== Component.Ready) + console.log("Double spin component is not ready !!") + } + + function addTextEdit(propName, currentText) + { + textEditComponent.createObject(scriptAlgoGrid, + {"propName": propName, "text": currentText }); + if (comboComponent.status !== Component.Ready) + console.log("TextEdit component is not ready !!") + } + + Component.onCompleted: + { + hueMatrixEditor.createScriptObjects(scriptAlgoGrid) + } + } + } + + // Script algorithm text label + Component + { + id: labelComponent + + RobotoText + { + implicitHeight: UISettings.listItemHeight + implicitWidth: width + property string propName + + label: propName + } + } + + // Script algorithm combo box property + Component + { + id: comboComponent + + CustomComboBox + { + Layout.fillWidth: true + property string propName + + onCurrentTextChanged: hueMatrixEditor.setScriptStringProperty(propName, currentText) + } + } + + // Script algorithm spin box property + Component + { + id: spinComponent + + CustomSpinBox + { + Layout.fillWidth: true + property string propName + + onValueModified: hueMatrixEditor.setScriptIntProperty(propName, value) + } + } + + // Script algorithm float box property + Component + { + id: doubleSpinComponent + + CustomDoubleSpinBox + { + Layout.fillWidth: true + property string propName + + decimals: 3 + suffix: "" + onRealValueChanged: hueMatrixEditor.setScriptFloatProperty(propName, realValue) + } + } + + // Script algorithm combo box property + Component + { + id: textEditComponent + + CustomTextEdit + { + Layout.fillWidth: true + property string propName + + onTextEdited: hueMatrixEditor.setScriptStringProperty(propName, text) + } + } +} diff --git a/qmlui/qmlui.qrc b/qmlui/qmlui.qrc index df24755cfc..b59916461f 100644 --- a/qmlui/qmlui.qrc +++ b/qmlui/qmlui.qrc @@ -136,6 +136,7 @@ qml/fixturesfunctions/ChaserEditor.qml qml/fixturesfunctions/SequenceEditor.qml qml/fixturesfunctions/RGBMatrixEditor.qml + qml/fixturesfunctions/HUEMatrixEditor.qml qml/fixturesfunctions/RGBMatrixPreview.qml qml/fixturesfunctions/EFXEditor.qml qml/fixturesfunctions/EFXPreview.qml diff --git a/qmlui/showmanager.cpp b/qmlui/showmanager.cpp index bfab16522a..26432308ec 100644 --- a/qmlui/showmanager.cpp +++ b/qmlui/showmanager.cpp @@ -1131,6 +1131,7 @@ bool ShowManager::insertShowItemTimeAt(ShowFunction *sf, int length, int cursorT case Function::CollectionType: case Function::EFXType: case Function::RGBMatrixType: + case Function::HUEMatrixType: { int newDuration = sf->duration() + length; if (newDuration < minDuration) @@ -1226,6 +1227,7 @@ bool ShowManager::cutShowItemTimeAt(ShowFunction *sf, int length, int cursorTime case Function::CollectionType: case Function::EFXType: case Function::RGBMatrixType: + case Function::HUEMatrixType: { int newDuration = int(sf->duration()) - targetCutDuration; if (newDuration < minDuration) diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 42ff4fe251..5d62a283f9 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(gobos) add_subdirectory(inputprofiles) add_subdirectory(miditemplates) add_subdirectory(modifierstemplates) +add_subdirectory(huescripts) add_subdirectory(rgbscripts) add_subdirectory(samples) if(qmlui) diff --git a/resources/huescripts/CMakeLists.txt b/resources/huescripts/CMakeLists.txt new file mode 100644 index 0000000000..fefc45f95d --- /dev/null +++ b/resources/huescripts/CMakeLists.txt @@ -0,0 +1,57 @@ +project(huescripts) + +set(SCRIPT_FILES + audioaurora.js + audiobarcode.js + audiobasslaser.js + audiobeatcolors.js + audioblocks.js + audioblurz.js + audiobuildup.js + audiocellular.js + audiochaser.js + audiocrawler.js + audiodjlight.js + audioenergy.js + audioenergy2.js + audioequalizer.js + audiofire.js + audiofireworks.js + audioflowfield.js + audioglitch.js + audioglitch2.js + audiogravimeter.js + audiohueshift.js + audiolava.js + audiomelt.js + audiomeltsparkle.js + audioplasma.js + audiopower.js + audiopuddles.js + audioreaction.js + audioreactor.js + audioscan.js + audioscanflare.js + audioscanmulti.js + audioshockwave.js + audioshot.js + audiosoap.js + audiospectrum.js + audiosplittower.js + audiostrobe.js + audiotunnel.js + audiovortex.js + audiowater.js + hsvutil.js +) + +# Only perform the copy if we're building for Android +if(ANDROID) + set(SCRIPTS_ASSETS_DIR "${QT_ANDROID_PACKAGE_SOURCE_DIR}/${INSTALLROOT}/${HUESCRIPTDIR}") + file(MAKE_DIRECTORY ${SCRIPTS_ASSETS_DIR}) + foreach(SCRIPT_FILE IN LISTS SCRIPT_FILES) + file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT_FILE} DESTINATION ${SCRIPTS_ASSETS_DIR}) + endforeach() +endif() + +install(FILES ${SCRIPT_FILES} DESTINATION ${INSTALLROOT}/${HUESCRIPTDIR}) diff --git a/resources/huescripts/audiospectrum.js b/resources/huescripts/audiospectrum.js index 677c34fedc..0f58e71dd4 100644 --- a/resources/huescripts/audiospectrum.js +++ b/resources/huescripts/audiospectrum.js @@ -18,7 +18,7 @@ var testAlgo; function () { var algo = new Object; algo.apiVersion = 3; - algo.name = "Audio Spectrum"; + algo.name = "Audio Spectrum Bars"; algo.author = "Ported from LedFx"; algo.acceptColors = 0; algo.usesAudio = true; diff --git a/resources/icons/svg/huematrix.svg b/resources/icons/svg/huematrix.svg new file mode 100644 index 0000000000..807a897076 --- /dev/null +++ b/resources/icons/svg/huematrix.svg @@ -0,0 +1,27 @@ + + + HUE Matrix + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/svg/svgicons.qrc b/resources/icons/svg/svgicons.qrc index 3e22d90d5c..8e7f1dd062 100644 --- a/resources/icons/svg/svgicons.qrc +++ b/resources/icons/svg/svgicons.qrc @@ -123,6 +123,7 @@ rename.svg resize.svg rgbmatrix.svg + huematrix.svg selectall.svg sequence.svg scanner.svg diff --git a/ui/src/app.cpp b/ui/src/app.cpp index 4427a06685..e5fcd7ca12 100644 --- a/ui/src/app.cpp +++ b/ui/src/app.cpp @@ -49,6 +49,7 @@ #include "qlcfixturedefcache.h" #include "audioplugincache.h" #include "rgbscriptscache.h" +#include "huescriptscache.h" #include "videoprovider.h" #include "qlcconfig.h" #include "qlcfile.h" @@ -515,6 +516,14 @@ void App::initDoc() m_doc->rgbScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory()); m_doc->rgbScriptsCache()->load(RGBScriptsCache::userScriptsDirectory()); + /* Load HUE scripts. HUEMatrix offers these plus every stock RGB script, + so the stock directories are registered here too, without the HSV + contract. */ + m_doc->hueScriptsCache()->load(HUEScriptsCache::systemScriptsDirectory(), true); + m_doc->hueScriptsCache()->load(HUEScriptsCache::userScriptsDirectory(), true); + m_doc->hueScriptsCache()->load(RGBScriptsCache::systemScriptsDirectory(), false); + m_doc->hueScriptsCache()->load(RGBScriptsCache::userScriptsDirectory(), false); + /* Load plugins */ connect(m_doc->ioPluginCache(), SIGNAL(pluginLoaded(const QString&)), this, SLOT(slotSetProgressText(const QString&))); diff --git a/unittest.sh b/unittest.sh index 879aedf0e4..6cb78b7b6e 100755 --- a/unittest.sh +++ b/unittest.sh @@ -20,6 +20,7 @@ cp -r $SOURCE_DIR/resources/gobos $DEST_DIR/resources cp -r $SOURCE_DIR/resources/icons $DEST_DIR/resources cp -r $SOURCE_DIR/resources/inputprofiles $DEST_DIR/resources cp -r $SOURCE_DIR/resources/rgbscripts $DEST_DIR/resources +cp -r $SOURCE_DIR/resources/huescripts $DEST_DIR/resources cp -r $SOURCE_DIR/resources/schemas $DEST_DIR/resources # Find all files necessary for tests recursively in the source directory and copy to destination directory diff --git a/variables.cmake b/variables.cmake index 25c212e941..ccadf1cfd8 100644 --- a/variables.cmake +++ b/variables.cmake @@ -365,6 +365,26 @@ elseif (IOS) set(USERRGBSCRIPTDIR "${USERDATADIR}/RGBScripts") endif () +# HUE Scripts (HSV contract, used by HUEMatrix) +if (WIN32) + set(HUESCRIPTDIR "HUEScripts") + set(USERHUESCRIPTDIR "${USERDATADIR}/HUEScripts") +elseif (APPLE) + set(HUESCRIPTDIR "${DATADIR}/HUEScripts") + set(USERHUESCRIPTDIR "${USERDATADIR}/HUEScripts") +else () + set(HUESCRIPTDIR "${DATADIR}/huescripts") + set(USERHUESCRIPTDIR "${USERDATADIR}/huescripts") +endif () + +if (ANDROID) + set(HUESCRIPTDIR "${DATADIR}/huescripts") + set(USERHUESCRIPTDIR "${USERDATADIR}/huescripts") +elseif (IOS) + set(HUESCRIPTDIR "HUEScripts") + set(USERHUESCRIPTDIR "${USERDATADIR}/HUEScripts") +endif () + # Web Files if (WIN32) set(WEBFILESDIR "Web")