Besides being an actual useful tool, this example demonstrates how to use several available classes and Touch Portal integration features. To create similar functionality would typically require a dedicated plugin in itself. But with DSE it can be created with some relatively simple scripting, and customized "on the fly" without editing configuration files or restarting Touch Portal for each change (also not needing yet another network client and process running in the background).
The script itself is mostly just an "interface" to the various color manipulation features available with a Color object. It stores an instance of a Color value, which is the "current color," and all operations are performed on this color, after which it is usually sent back to Touch Portal as a new color value to be displayed somewhere (like the central "swatch" in the example page).
The example page uses 7 Sliders as inputs to control various aspects of the color (color channel values and opacity). Each color channel is controlled by a Slider and also the corresponding up/down buttons. The color can be changed from the same screen using either RGB or HSV channels. When a channel is modified in one color model (eg. Red), the changes are reflected in the other model's sliders, eg. moving the hue/saturation/value to corresponding positions.
The up/down buttons for each input value are there because Touch Portal Sliders only have a resolution of 0 through 100 in whole steps (1, 2, 3, etc), whereas most of the actual color values can be in a range of 0 through 255 (or 0-360 for Hue and fractional percent for saturation and value). So, the buttons provide a way to "fine tune" the value to account for the missing resolution of the sliders.
An additional Slider is provided to rotate the swatch overlay text color, or to set it to automatic (white or black text depending on the overall darkness of the main color).
Additionally there are buttons to invoke clipboard functions to copy the current color to clipboard in various formats, and another button to set the current color from a value on the clipboard (it has to be in one of the recognized formats... see setColor()
notes in the code below).
Last but not least there are options to show colors complementary to the current one, or select one of a "series" of colors reflecting various aspects of the current main color, such as harmonious or analogous colors. These additional colors can be generated on demand by pressing the corresponding buttons, and/or set to automatically update when the main color changes by toggling the "Auto Update" feature.
Pressing the color complement/series color swatches can either copy that color to the clipboard, or select it as the current main color, depending on the "ON PRESS" switch position.
The background image is in a 16:9 aspect ratio. This should generally work for any screen resolution or aspect. The exception may be the horizontal marks next to the sliders, which may not properly reflect the actual slider extents on screens with different aspect ratios, or if the Touch Portal title/tool bar is displayed (eg. on iOS or Android with "full screen" feature disabled).
An alternate background image, without the slider position markings, is available here: background_nomarks.jpg
The default background image, with the slider position markings, is available here: background.jpg
The background image source design in Illustrator format is available here: background.ai
You may notice that button on the bottom row with the technicolor dropper icon. This is meant to launch a "color picker" utility on your computer, which would then copy the color from any part of the screen you click on to the clipboard. This utility is going to be operating system-specific, and you may prefer one over another.
The current button on the page is set up to launch the Windows 10/11 "Power Toys" Color Picker utility by pressing its default shortcut key combination of WIN+SHIFT+C
. (Power Toys is a collection of utilities from a Microsoft-supported but independent open-source project.) You may of course prefer some other utility, and if you're on MacOS or Linux then clearly you'll need some other option.
After the button is pressed, the script will wait up to 30 seconds for the clipboard contents to change. If the clipboard is updated, the new clipboard value is checked to see if it contains a valid color. And if it does, then the main selected color is changed to that value.
18import {
setText as setClipboardText,
text as clipboardText,
textAvailable as clipboardTextAvailable, clipboardChanged } from
"clipboard";
20export { setClipboardText as copy };
25export var STATES_GROUP_NAME =
"Color Mixer";
33export var FORMATTED_COLOR_TEMPLATE =
"R: {8:000}\t\tH {12:000}°\nG: {9:000}\t\tS {13:000}\nB: {10:000}\t\tV {14:000}\nA: {11:000}\t\tL {15:000}\n\n\t{8:X2} {9:X2} {10:X2} {11:X2}";
37export
function color()
46 return color().tpcolor();
55export
function setColor(color)
57 _data.color =
new Color(color);
63export
function setColorArgb(color)
65 const newColor = colorFromArgb(color);
66 if (newColor.isValid())
67 return setColor(newColor);
68 console.error(LOG_PREFIX, `
'${color}' is not a valid color.`);
80export
function setRed(percent, updateConnector =
false)
82 color()._r = percentToShort(percent);
84 updateRedConnector(color()._r);
88export
function setGreen(percent, updateConnector =
false)
90 color()._g = percentToShort(percent);
92 updateGreenConnector(color()._g);
96export
function setBlue(percent, updateConnector =
false)
98 color()._b = percentToShort(percent);
100 updateBlueConnector(color()._b);
104export
function setAlpha(alpha, updateConnector =
false)
106 color().setAlpha(alpha * 0.01);
108 updateAlphaConnector(color().alpha());
112export
function setHue(percent, updateConnector =
false)
114 var hsv = color().toHsv();
115 hsv.h = percentToDegrees(percent);
116 _data.color =
new Color(hsv);
118 updateHueConnector(hsv.h);
122export
function setSaturation(percent, updateConnector =
false)
126 var hsv = color().toHsv();
128 _data.color =
new Color(hsv);
130 updateSatConnector(hsv.s);
134export
function setValue(percent, updateConnector =
false)
138 var hsv = color().toHsv();
140 _data.color =
new Color(hsv);
142 updateValueConnector(hsv.v);
147export
function setLightness(percent)
151 var hsl = color().toHsl();
153 _data.color =
new Color(hsl);
166export
function adjustRed(amount)
168 return setRed(shortToPercent(wrapShort(color()._r, amount)),
true);
171export
function adjustGreen(amount)
173 return setGreen(shortToPercent(wrapShort(color()._g, amount)),
true);
176export
function adjustBlue(amount)
178 return setBlue(shortToPercent(wrapShort(color()._b, amount)),
true);
181export
function adjustAlpha(amount)
183 return setAlpha(wrapPercent(color().alpha(), shortToPercent(amount)) * 100,
true);
187export
function adjustHue(amount)
189 return setHue(degreesToPercent(wrapDegrees(color().toHsv().h, amount)),
true);
194export
function adjustSaturation(amount)
196 return setSaturation(wrapPercent(color().toHsv().s, amount),
true);
199export
function adjustValue(amount)
201 return setValue(wrapPercent(color().toHsv().v, amount),
true);
205export
function adjustLightness(amount)
207 return setLightness(wrapPercent(color().toHsl().l, amount));
215export
function textColor()
217 return _data.textColor;
222export
function setTextColor(color)
224 _data.textColor =
new Color(color);
225 _data.textColorAuto = (
Color.equals(COLOR_WHITE, _data.textColor) ||
Color.equals(COLOR_BLACK, _data.textColor));
227 _data.instance.dataStore.textColor = _data.textColor.rgba();
228 _data.instance.dataStore.textColorAuto = _data.textColorAuto;
235export
function setTextHue(percent)
237 _data.textColorAuto = !percent;
238 if (_data.textColorAuto) {
244 if (
Color.equals(COLOR_WHITE, textColor()) ||
Color.equals(COLOR_BLACK, textColor()))
245 _data.textColor =
new Color({ r: 255, g: 0, b: 0 });
246 var hsl = textColor().toHsl();
247 hsl.h = percentToDegrees(percent);
253function updateTextColor()
255 if (!_data.textColorAuto)
257 if (color().isLight()) {
258 if (!
Color.equals(COLOR_BLACK, textColor()))
259 setTextColor(COLOR_BLACK.clone());
262 if (!
Color.equals(COLOR_WHITE, textColor()))
263 setTextColor(COLOR_WHITE.clone());
268function sendTextColor() {
280 sendColorsArray([color().
complement()],
"complement");
285export
function splitcomplement()
287 sendColorsArray(color().splitcomplement().slice(1),
"splitcomp");
293 _data.lastSeries = {t:
"polyad", n: n, s: 0 };
295 sendCurrentSeriesNameState();
299export
function analogous(n, slices = 30)
301 _data.lastSeries = {t:
"analogous", n: n, s: slices };
303 sendCurrentSeriesNameState();
307export
function monochromatic(n)
309 _data.lastSeries = {t:
"monochromatic", n: n, s: 0 };
311 sendCurrentSeriesNameState();
318export
function clearCurrentSeries(n = -1)
320 _data.lastSeries.t =
"";
323 n = _data.seriesMaxColors;
324 _data.lastSeries.n = n;
327 sendCurrentSeriesNameState();
331export
function autoUpdateSeries(enable =
true)
333 if (_data.autoUpdateSeries === enable)
336 _data.autoUpdateSeries = enable;
338 sendAutoUpdateSeriesState();
339 _data.instance.dataStore.autoUpdateSeries = _data.autoUpdateSeries;
343export
function autoUpdateSeriesToggle()
345 autoUpdateSeries(!_data.autoUpdateSeries);
351export
function toggleSeriesColorPressMode()
353 if (_data.seriesColorPressMode ==
"select")
354 _data.seriesColorPressMode =
"copy";
356 _data.seriesColorPressMode =
"select";
358 _data.instance.dataStore.seriesColorPressMode = _data.seriesColorPressMode;
360 sendSeriesColorPressModeState();
369 _data.instance.dataStore.lastSeries = _data.lastSeries;
370 switch(_data.lastSeries.t) {
372 sendColorsArray(color().
polyad(_data.lastSeries.n+1).slice(1));
375 sendColorsArray(color().analogous(_data.lastSeries.n+1, _data.lastSeries.s).slice(1));
377 case 'monochromatic':
378 sendColorsArray(color().monochromatic(_data.lastSeries.n+1).slice(1));
382 [...Array(_data.lastSeries.n).keys()].forEach(() => colors.push(COLOR_TRANS));
383 sendColorsArray(colors);
395function sendColorsArray(arry, stateNamePrefix =
"")
402 const maxIdx = arry.length - 1;
403 const maxColors = stateNamePrefix ? maxIdx + 1 : _data.seriesMaxColors;
404 for (let i=0; i < maxColors; ++i) {
406 let stateId = _data.stateId +
'_';
408 stateId += stateNamePrefix;
412 stateId += `_${i+1}`;
414 if (_data.colorSeriesStates.indexOf(stateId) < 0) {
415 let stateDescript = STATES_GROUP_NAME +
" ";
417 stateDescript += stateNamePrefix;
419 stateDescript +=
"series color";
421 stateDescript += ` ${i+1}`;
422 TP.
stateCreate(stateId, STATES_GROUP_NAME, stateDescript,
"#00000000");
423 _data.colorSeriesStates.push(stateId);
428 arry.push(COLOR_TRANS);
434function updateSeries()
436 if (!_data.autoUpdateSeries)
445function sendCurrentSeriesNameState()
450 if (_data.lastSeries.t) {
451 seriesName = `${_data.lastSeries.t}(${_data.lastSeries.n}`;
452 if (_data.lastSeries.s > 0)
453 seriesName +=
", " + _data.lastSeries.s;
460function sendAutoUpdateSeriesState()
462 TP.
stateUpdateById(_data.autoUpdateEnabledState, _data.autoUpdateSeries ?
"1" :
"0");
466function sendSeriesColorPressModeState()
479export
function fromClipboard()
481 const val = clipboardText();
483 const color =
new Color(val);
485 return setColor(color);
486 console.error(LOG_PREFIX,
"Clipboard contents were not a valid color.");
495export
function monitorClipboard(timeout = 30)
497 if (!_data.clipboardMonitorTimer) {
498 _data.clipboardMonitorTimer = setTimeout(checkClipboard, timeout * 1000,
true);
499 clipboardChanged.connect(checkClipboard);
500 console.info(LOG_PREFIX,
"Clipboard monitor started with " + timeout +
" second timeout.");
504function checkClipboard(timeout =
false)
506 if (_data.clipboardMonitorTimer) {
507 clearTimeout(_data.clipboardMonitorTimer);
508 _data.clipboardMonitorTimer = 0;
509 clipboardChanged.disconnect(checkClipboard);
510 if (!timeout && clipboardTextAvailable())
512 console.info(LOG_PREFIX,
"Clipboard monitor " + (timeout ?
"timed out." :
"checked clipboard."));
521export
function argb2rgba(argbString)
523 return argbString.length === 9 ? argbString.replace(/^#([0-9a-z]{2})([0-9a-z]{6})$/i,
'#$2$1') : argbString;
527export
function colorFromArgb(argbString)
529 return new Color(argb2rgba(argbString));
534export
function hexColor(color)
536 const c =
new Color(color);
544export
function hexFromArgb(argbString)
546 return hexColor(colorFromArgb(argbString));
565function getShortId(channel)
567 if (!_data.connectorIds[channel] && _data.instance) {
568 const shortIds =
TP.
getConnectorShortIds({ instanceName: _data.instance.name, expression: `*
set${channel}*` });
570 _data.connectorIds[channel] = shortIds[0];
573 return _data.connectorIds[channel];
577function updateConnector(channel, value)
579 const shortId = getShortId(channel);
581 TP.connectorUpdateShort(shortId, value.clamp(0, 100));
585function updateRedConnector(r) { updateConnector(
"Red",
round(shortToPercent(r))); }
586function updateGreenConnector(g) { updateConnector(
"Green",
round(shortToPercent(g))); }
587function updateBlueConnector(b) { updateConnector(
"Blue",
round(shortToPercent(b))); }
588function updateAlphaConnector(a) { updateConnector(
"Alpha",
round(a * 100)); }
589function updateHueConnector(h) { updateConnector(
"Hue",
round(degreesToPercent(h))); }
590function updateSatConnector(s) { updateConnector(
"Saturation",
round(s * 100)); }
591function updateValueConnector(v) { updateConnector(
"Value",
round(v * 100)); }
592function updateTextHueConnector(h) { updateConnector(
"TextHue",
round(degreesToPercent(h))); }
595function updateRgbConnectors()
597 const rgb = color().toRgb();
598 updateRedConnector(rgb.r);
599 updateGreenConnector(rgb.g);
600 updateBlueConnector(rgb.b);
604function updateHsvConnectors()
606 const hsv = color().toHsv();
607 updateHueConnector(hsv.h);
608 updateSatConnector(hsv.s);
609 updateValueConnector(hsv.v);
621function update(connectors = 0)
628 _data.instance.dataStore.color = _data.color.rgba();
631 const rgb = _data.color.toRgb();
632 const hsv = _data.color.toHsv();
633 const lum = _data.color.luminance();
636 FORMATTED_COLOR_TEMPLATE,
637 shortToPercent(rgb.r) * .01, shortToPercent(rgb.g) * .01, shortToPercent(rgb.b) * .01, rgb.a, degreesToPercent(hsv.h) * .01, hsv.s, hsv.v, lum,
638 rgb.r, rgb.g, rgb.b, percentToShort(rgb.a * 100), hsv.h, percentToShort(hsv.s * 100), percentToShort(hsv.v * 100), percentToShort(lum * 100)
644 for (
const [k, v] of Object.entries(_data.channelValues)) {
647 const newVal = v.get(rgb, hsv, lum);
655 updateRgbConnectors();
657 updateHsvConnectors();
659 updateAlphaConnector(rgb.a);
665function updateRepeatRate(ms) {
668function updateRepeatDelay(ms) {
677 console.info(LOG_PREFIX,
"initializing...");
681 if (!_data.instance) {
682 console.error(
"Something went wrong, could not find current DynamicScript instance!");
686 _data.stateId = _data.instance.stateId;
688 _data.instance.stateParentCategory = STATES_GROUP_NAME;
691 const ds = _data.instance.dataStore;
694 console.info(LOG_PREFIX,
"restoring saved state");
695 _data.color =
new Color(ds.color);
696 _data.textColor =
new Color(ds.textColor);
697 _data.textColorAuto = ds.textColorAuto;
698 _data.lastSeries = ds.lastSeries;
699 _data.autoUpdateSeries = ds.autoUpdateSeries;
700 if (ds.seriesColorPressMode)
701 _data.seriesColorPressMode = ds.seriesColorPressMode;
704 console.info(LOG_PREFIX,
"no saved state found, creating new data store");
705 ds.color = _data.color.rgba();
706 ds.textColor = _data.textColor.rgba();
707 ds.textColorAuto = _data.textColorAuto;
708 ds.lastSeries = _data.lastSeries;
709 ds.autoUpdateSeries = _data.autoUpdateSeries;
710 ds.seriesColorPressMode = _data.seriesColorPressMode;
714 _data.formattedColorState = _data.stateId +
'_formatted';
715 let stateDescript = STATES_GROUP_NAME +
" - Current Color";
717 TP.
stateCreate(_data.stateId, STATES_GROUP_NAME, stateDescript,
"");
720 _data.formattedColorState = _data.stateId +
'_formatted';
721 stateDescript = STATES_GROUP_NAME +
" Formatted Color";
722 TP.
stateCreate(_data.formattedColorState, STATES_GROUP_NAME, stateDescript,
"");
725 for (
const [k, v] of Object.entries(_data.channelValues)) {
727 v.state = _data.stateId +
'_channel_' + k;
728 stateDescript = STATES_GROUP_NAME +
" Channel Value: " + v.name;
729 TP.
stateCreate(v.state, STATES_GROUP_NAME, stateDescript,
"");
733 _data.textColorState = _data.stateId +
'_textColor';
734 stateDescript = STATES_GROUP_NAME +
" Text Color";
736 TP.
stateCreate(_data.textColorState, STATES_GROUP_NAME, stateDescript, _data.textColor.lighten().rgba());
739 _data.seriesColorPressModeState = _data.stateId +
'_seriesColorPressMode';
740 stateDescript = STATES_GROUP_NAME +
" Series Color Swatch Press Mode";
741 TP.
stateCreate(_data.seriesColorPressModeState, STATES_GROUP_NAME, stateDescript,
"");
744 _data.lastSeriesNameState = _data.stateId +
'_selectedSeries';
745 stateDescript = STATES_GROUP_NAME +
" Selected Color Series";
747 TP.
stateCreate(_data.lastSeriesNameState, STATES_GROUP_NAME, stateDescript,
"");
750 _data.autoUpdateEnabledState = _data.stateId +
'_autoUpdateSeries';
751 stateDescript = STATES_GROUP_NAME +
" Auto Update Complement/Series";
753 TP.
stateCreate(_data.autoUpdateEnabledState, STATES_GROUP_NAME, stateDescript,
"");
756 _data.repeatRateState = _data.stateId +
'_actionRepeatRate';
757 stateDescript = STATES_GROUP_NAME +
" Color Channel Button Repeat Rate";
758 TP.
stateCreate(_data.repeatRateState, STATES_GROUP_NAME, stateDescript,
"0");
759 _data.repeatDelayState = _data.stateId +
'_actionRepeatDelay';
760 stateDescript = STATES_GROUP_NAME +
" Color Channel Button Repeat Delay";
761 TP.
stateCreate(_data.repeatDelayState, STATES_GROUP_NAME, stateDescript,
"0");
764 _data.instance.repeatRateChanged.connect(updateRepeatRate);
765 _data.instance.repeatDelayChanged.connect(updateRepeatDelay);
775 TP.onconnectorIdsChanged( (instanceName) => {
776 if (_data.instance && instanceName == _data.instance.name) {
777 _data.connectorIds = {};
778 console.info(LOG_PREFIX,
"Connector ID cache cleared.");
784 sendCurrentSeriesNameState();
785 sendAutoUpdateSeriesState();
786 sendSeriesColorPressModeState();
787 updateRepeatRate(_data.instance.effectiveRepeatRate);
788 updateRepeatDelay(_data.instance.effectiveRepeatDelay);
790 console.info(LOG_PREFIX,
"initialization completed.");
794const COLOR_BLACK =
new Color({ r: 0, g: 0, b: 0 });
795const COLOR_WHITE =
new Color({ r: 255, g: 255, b: 255 });
796const COLOR_TRANS =
new Color({ r: 0, g: 0, b: 0, a: 0 });
797const LOG_PREFIX =
"DSE Color Mixer:";
803 color: COLOR_WHITE.clone(),
804 textColor: COLOR_BLACK.clone(),
808 seriesColorPressMode:
'select',
809 autoUpdateSeries:
false,
810 lastSeries: { t:
"", n: 0, s: 0 },
812 colorSeriesStates: [],
813 formattedColorState:
"",
815 seriesColorPressModeState:
"",
816 lastSeriesNameState:
"",
817 autoUpdateEnabledState:
"",
819 repeatDelayState:
"",
825 r: { name:
"Red", v: -1, state:
"",
get: (rgb, _, __) => shortToPercent(rgb.r), fmt:
"{0:00.0} %" },
826 g: { name:
"Green", v: -1, state:
"",
get: (rgb, _, __) => shortToPercent(rgb.g), fmt:
"{0:00.0} %" },
827 b: { name:
"Blue", v: -1, state:
"",
get: (rgb, _, __) => shortToPercent(rgb.b), fmt:
"{0:00.0} %" },
828 a: { name:
"Alpha", v: -1, state:
"",
get: (rgb, _, __) => rgb.a * 100, fmt:
"{0:00} %" },
829 h: { name:
"Hue", v: -1, state:
"",
get: (_, hsv, __) => hsv.h, fmt:
"{0:000} °" },
830 s: { name:
"Saturation", v: -1, state:
"",
get: (_, hsv, __) => hsv.s * 100, fmt:
"{0:00.0} %" },
831 v: { name:
"Value", v: -1, state:
"",
get: (_, hsv, __) => hsv.v * 100, fmt:
"{0:00.0} %" },
832 l: { name:
"Lightness", v: -1, state:
"",
get: (_, __, l) => l * 100, fmt:
"{0:00.0} %" },
843function percentToShort(value) {
return round(value.clamp(0, 100) * 2.55); }
844function shortToPercent(value) {
return value.clamp(-255, 255) / 2.55 }
845function percentToDegrees(value) {
return round(value.clamp(0, 100) * 3.6); }
846function degreesToPercent(value) {
return value.clamp(-360, 360) / 3.6; }
849function wrapShort(v, d) {
850 var p = (v + d.clamp(-255, 255)) % 256;
851 return (p < 0 ? 256 + p : p);
855function wrapDegrees(v, d) {
856 var p = (v + d.clamp(-360, 360)) % 360;
857 return (p < 0 ? 360 + p : p);
861function wrapPercent(v, d) {
862 var p = v +
clamp(d * 0.01, -1, 1);
String setText(String text, int mode=Mode.Clipboard)
This function sets the current system clipboard value as a plain text String type (ASCII/UTF8/UTF16)....
bool textAvailable(int mode=Mode.Clipboard)
Returns true if the current system clipboard contains any type of string data, or false otherwise....
Provides functions for working with color.
Color complement(Color color)
Returns a new Color which is complimentary to color.
Color polyad(number n)
Returns an array of up to n colors harmonious with this color.
string tpcolor()
Returns color formatted in #AARRGGBB format (this is suitable for use with Touch Portal's "Set Text/B...
Definition: color.js:474
The DSE object contains constants and functions related to the plugin environment....
DynamicScript currentInstance()
Returns the current script Instance. This is equivalent to calling DSE.instance(DSE....
Definition: DSE.h:292
DynamicScript instance(String name)
Returns the script instance with given name, if any, otherwise returns null.
String Format(String format)
Equivalent to the String.format() extension method.
number clamp(number value, number min, number max)
Constrain value between min and max values.
number round(number precision)
Round value to precision decimal places.
< Promise|string > text()
The global TP namespace is used as a container for all Touch Portal API methods, which are invoked wi...
Definition: touchportal.dox:7
Array< String > getConnectorShortIds(Object criteria={})
Returns zero or more connector "short IDs" based on the given criteria from the currently logged conn...
void stateCreate(String id, String parentGroup, String desc="", String defaultValue="")
Create a new Touch Portal dynamic State (if it doesn't already exist).
void stateUpdateById(String id, String value)
Send a State update to Touch Portal for any arbitrary state/value matching the id,...