v1.2.0.1-beta1
Color Mixer Example

A script and page example of an interface for choosing a color.

See the published documentation for a properly formatted version of this README.

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).

Plugin components and features used:

  • Color library for the primary functionality (choosing and previewing colors).
  • Using Touch Portal Sliders (connectors), both for input (to change color) and as a visual indicator of current channel values (move related sliders as color changes).
  • Saving and restoring the last "state" of the color mixer page when Touch Portal/the plugin shut down and restart, including the last selected color, series, and the other options presented on the page.
  • Clipboard features provided by the Clipboard module for copying colors to/from other applications and monitoring the clipboard for changes after a local color picker utility has been launched.
  • Using separate Script instances for the color adjustment buttons which act independently of each other, with adjustable repeat rate and delay settings.

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.

All of the States necessary for all this functionality, are created dynamically in the script as needed. For example the color series States are only created if that feature of the page is used. Each individual color channel value (R/G/B/A/H/S/V) are sent as individual States, which in this page are displayed under the corresponding sliders.

Note
Assets for this example, including the code and page shown below, can be found in the project's repository at
https://github.com/mpaperno/DSEP4TP/tree/next/resources/examples/ColorMixer/

Example page using this script

Initial page setup

  1. Download the DSE_Color_Mixer_v1 page.
  2. Import the page into Touch Portal
    • The script file used in the example are already included in the page's archive and will be copied into your Touch Portal data directory, into the "misc" subfolder.
  3. Create a new button somewhere (eg. on your (main) page) that opens this page on your Touch Portal Android/iOS device.
  4. Navigate to the new page on your device, and then press the INITIALIZE button.
    • This only needs to be done once when you first import the page. After that everything should be re-created automatically next time you start Touch Portal (or restart this plugin).
  5. The "INITIAL SETUP" button should turn into a white color swatch and the page is ready for use.
  6. If you make any changes to the included script file, you need to use the "Reset Module" button to clear out the old script version before your changes will be detected.

Background Image

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

The "color picker" button

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.

Color Mixer script

1/* Dynamic Script Engine - Color Mixer Script
2
3 This script is meant to accompany the Color Mixer example Touch Portal page which should be distributed alongside.
4
5 This is in a lot of ways like a mini-plugin in itself, as it provides a lot of functionality one would otherwise need
6 an actual Touch Portal plugin for.
7
8The code is organized starting with basic functions first, such as for setting and adjusting colors, and progressing to "extra"
9functionality like providing the color series options and so on. At the end are the "internal" bits, including data storage,
10initialization, and utility functions.
11
12Any function or variable marked as `export` can be used in a Touch Portal DSE action
13or imported into another script/module with `import` statement or `require()` function.
14
15*/
16
17// Import clipboard functions from the built-in "clipboard" module (documented in JavaScript Library Reference -> Utilities -> Clipboard ).
18import { setText as setClipboardText, text as clipboardText, textAvailable as clipboardTextAvailable, clipboardChanged } from "clipboard";
19// Export the clipboard copy function as simply 'copy()' so it is available directly from TP actions (as `M.copy()`) w/out another import.
20export { setClipboardText as copy };
21
22// This script creates Touch Portal States dynamically as needed.
23// This variable sets the title under which all the created States are grouped.
24// The group will appear as a child of the Dynamic Script Engine main plugin grouping in TP menus.
25export var STATES_GROUP_NAME = "Color Mixer";
26
27// Template for formatted color value. On the Color Mixer page this is shown on top of the primary color swatch.
28// The template uses .NET String.Format() style numeric placeholders (number before ':') and format specifiers (following the ':').
29// Template placeholder values are ("real" means 0-1 range, "int" means 0-255 (or 0-359 for Hue)):
30// 0 = red (real); 1 = blue (real); 2 = green (real); 3 = alpha (real); 4 = hue (real); 5 = saturation (real); 6 = value (real); 7 = lightness (real);
31// 8 = red (int); 9 = blue (int); 10 = green (int); 11 = alpha (int); 12 = hue (° int); 13 = saturation (int); 14 = value (int); 15 = lightness (int)
32//export var FORMATTED_COLOR_TEMPLATE = "{0:F2} {1:F2} {2:F2} {3:F2}\n{4:F2} {5:F2} {6:F2} {7:F2}\n{8:X2} {9:X2} {10:X2} {11:X2}\n{12:000} {13:000} {14:000} {15:000}"; // uncomment this line to see what all the fields look like
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}";
34
35// Returns the current Color object, on which various methods can be invoked directly.
36// See documentation at: https://dse.tpp.max.paperno.us/class_color.html
37export function color()
38{
39 return _data.color;
40}
41
42// Returns current color in "Touch Portal" format of #AARRGGBB format,
43// suitable for use in an "Change visuals by plug-in state" action.
44export function tpcolor()
45{
46 return color().tpcolor();
47}
48
49// Sets the current color to `color` which can be:
50// - An hex string in "#RRGGBB[AA]" format, with or w/out the leading "#";
51// - A color name from CSS specification, eg "red", "darkblue", etc.;
52// - Any other valid CSS-style color specifier string, eg. "rgba(255, 0, 0, .5)" or "hsl(0, 100%, 50%)" or "hsva(0, 100%, 50%, .5)", etc.;
53// - An object representing rgb[a]/hsl[a]/hsv[a] values, eg. `{ r: 255, g: 0, b: 0 }` or `{ h: 0, s: 1, l: .5, a: .75 }` or `{ h: 0, s: 100, v: 100 }`;
54// - Another Color object instance.
55export function setColor(color)
56{
57 _data.color = new Color(color);
58 return update(3);
59}
60
61// Sets the current color from an "#AARRGGBB" format string (this format is used by Touch Portal
62// for button background colors in "Change visuals by plug-in state" actions).
63export function setColorArgb(color)
64{
65 const newColor = colorFromArgb(color);
66 if (newColor.isValid())
67 return setColor(newColor);
68 console.error(LOG_PREFIX, `'${color}' is not a valid color.`);
69 return tpcolor();
70}
71
72
73// ------------------------------------------
74// Color channel/component setting functions.
75
76// The following functions set a corresponding channel of the current color to a percentage value (0 - 100).
77// If `updateConnector` is true then the corresponding slider will also be repositioned (see adjustment functions below;
78// when adjusting using a slider, this should be `false`).
79
80export function setRed(percent, updateConnector = false)
81{
82 color()._r = percentToShort(percent);
83 if (updateConnector)
84 updateRedConnector(color()._r);
85 return update(2);
86}
87
88export function setGreen(percent, updateConnector = false)
89{
90 color()._g = percentToShort(percent);
91 if (updateConnector)
92 updateGreenConnector(color()._g);
93 return update(2);
94}
95
96export function setBlue(percent, updateConnector = false)
97{
98 color()._b = percentToShort(percent);
99 if (updateConnector)
100 updateBlueConnector(color()._b);
101 return update(2);
102}
103
104export function setAlpha(alpha, updateConnector = false)
105{
106 color().setAlpha(alpha * 0.01);
107 if (updateConnector)
108 updateAlphaConnector(color().alpha());
109 return update(0); // alpha is only controlled by one slider, no other connector updates needed.
110}
111
112export function setHue(percent, updateConnector = false)
113{
114 var hsv = color().toHsv();
115 hsv.h = percentToDegrees(percent);
116 _data.color = new Color(hsv);
117 if (updateConnector)
118 updateHueConnector(hsv.h);
119 return update(1);
120}
121
122export function setSaturation(percent, updateConnector = false)
123{
124 if (percent === 1)
125 percent += .01;
126 var hsv = color().toHsv();
127 hsv.s = percent;
128 _data.color = new Color(hsv);
129 if (updateConnector)
130 updateSatConnector(hsv.s);
131 return update(1);
132}
133
134export function setValue(percent, updateConnector = false)
135{
136 if (percent === 1)
137 percent += .01;
138 var hsv = color().toHsv();
139 hsv.v = percent;
140 _data.color = new Color(hsv);
141 if (updateConnector)
142 updateValueConnector(hsv.v);
143 return update(1);
144}
145
146// Note that this changes the color space of the current color to HSL.
147export function setLightness(percent)
148{
149 if (percent === 1)
150 percent += .01;
151 var hsl = color().toHsl();
152 hsl.l = percent;
153 _data.color = new Color(hsl);
154 return update(3);
155}
156
157
158// ------------------------------------------
159// Adjustment (step increment/decrement) functions.
160
161// The following functions are used to adjust the individual color channels by a set +/- amount from what they currently are.
162// They will wrap the result values into a range between the minimum and maximum values of the corresponding channel (eg. for red: 255 + 1 = 0).
163
164// For the RGBA channels the steps are integer values in the range of (-255 - +255).
165
166export function adjustRed(amount)
167{
168 return setRed(shortToPercent(wrapShort(color()._r, amount)), true);
169}
170
171export function adjustGreen(amount)
172{
173 return setGreen(shortToPercent(wrapShort(color()._g, amount)), true);
174}
175
176export function adjustBlue(amount)
177{
178 return setBlue(shortToPercent(wrapShort(color()._b, amount)), true);
179}
180
181export function adjustAlpha(amount)
182{
183 return setAlpha(wrapPercent(color().alpha(), shortToPercent(amount)) * 100, true);
184}
185
186// The hue channel steps are integer values in the range of (-360 - +360).
187export function adjustHue(amount)
188{
189 return setHue(degreesToPercent(wrapDegrees(color().toHsv().h, amount)), true);
190}
191
192// The S/V/L channel steps are percentage values in the range of (-100 - +100).
193
194export function adjustSaturation(amount)
195{
196 return setSaturation(wrapPercent(color().toHsv().s, amount), true);
197}
198
199export function adjustValue(amount)
200{
201 return setValue(wrapPercent(color().toHsv().v, amount), true);
202}
203
204// Note that this changes the color space of the current color to HSL.
205export function adjustLightness(amount)
206{
207 return setLightness(wrapPercent(color().toHsl().l, amount));
208}
209
210
211// ------------------------------------------
212// Text color functions
213
214// Returns the current text Color object.
215export function textColor()
216{
217 return _data.textColor;
218}
219
220// Sets the current text color and updates the "Text Color" State with the color as an "#RRGGBB" format string.
221// See `setColor()` for allowed format specifics.
222export function setTextColor(color)
223{
224 _data.textColor = new Color(color);
225 _data.textColorAuto = (Color.equals(COLOR_WHITE, _data.textColor) || Color.equals(COLOR_BLACK, _data.textColor));
226 // Save the last used color and auto setting to persistent storage object (this is saved/restored with the Script Instance when plugin exits/starts)
227 _data.instance.dataStore.textColor = _data.textColor.rgba();
228 _data.instance.dataStore.textColorAuto = _data.textColorAuto;
229 sendTextColor();
230}
231
232// Set the hue component of the current text color to a percentage value (0 - 100) and returns the new color as an #AARRGGBB string.
233// If `percent` is zero, the text color is placed into "auto" mode which will show either light or dark text based on current main
234// color value (dark text on light color and vice versa).
235export function setTextHue(percent)
236{
237 _data.textColorAuto = !percent;
238 if (_data.textColorAuto) {
239 updateTextColor();
240 return;
241 }
242
243 // need to reset pure white or black colors to some primary color before shifting hue.
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);
248 setTextColor(hsl);
249}
250
251// Internal helper to automatically set the text color based on current main color.
252// Only used when in "auto text color" mode.
253function updateTextColor()
254{
255 if (!_data.textColorAuto)
256 return;
257 if (color().isLight()) {
258 if (!Color.equals(COLOR_BLACK, textColor()))
259 setTextColor(COLOR_BLACK.clone());
260 }
261 else {
262 if (!Color.equals(COLOR_WHITE, textColor()))
263 setTextColor(COLOR_WHITE.clone());
264 }
265}
266
267// Send a State update with the new text color value.
268function sendTextColor() {
269 TP.stateUpdateById(_data.textColorState, textColor().hex());
270}
271
272
273// ------------------------------------------
274// Color complement/series array generators.
275
276// Generates a color complementary to the current color.
277// Updates/creates 1 State based on current instance name + "_complement".
278export function complement()
279{
280 sendColorsArray([color().complement()], "complement");
281}
282
283// Generates 2 colors which are split-complementary to the current color.
284// Updates/creates 2 States based on current instance name + "_splitcomp_1" and + "_splitcomp_2".
285export function splitcomplement()
286{
287 sendColorsArray(color().splitcomplement().slice(1), "splitcomp");
288}
289
290// Generates up to `n` number of colors harmonious with the current color. Updates/creates States based on current instance name and numeric suffix.
291export function polyad(n)
292{
293 _data.lastSeries = {t: "polyad", n: n, s: 0 };
294 sendSeries();
295 sendCurrentSeriesNameState();
296}
297
298// Generates up to `n` number of colors analogous to the current color. Updates/creates States based on current instance name and numeric suffix.
299export function analogous(n, slices = 30)
300{
301 _data.lastSeries = {t: "analogous", n: n, s: slices };
302 sendSeries();
303 sendCurrentSeriesNameState();
304}
305
306// Generates up to `n` number of monochromatic colors based on the current color. Updates/creates States based on current instance name and numeric suffix.
307export function monochromatic(n)
308{
309 _data.lastSeries = {t: "monochromatic", n: n, s: 0 };
310 sendSeries();
311 sendCurrentSeriesNameState();
312}
313
314// This function clears the current color series (polyad, analogous, mono),
315// sending an array of `n` transparent colors instead. If `n` is -1 (default) then the count from the last
316// requested series is used. If `n` is 0, then no transparent colors are sent (the last series color state values stay intact).
317// Can be used to simply clear any current series colors or to keep automatic updates of complement/split comp but w/out series updates.
318export function clearCurrentSeries(n = -1)
319{
320 _data.lastSeries.t = "";
321 if (n) {
322 if (n < 0)
323 n = _data.seriesMaxColors;
324 _data.lastSeries.n = n;
325 sendSeries();
326 }
327 sendCurrentSeriesNameState();
328}
329
330// Enables or disables the automatic updating of complement, split complement, and last selected series based on the current color.
331export function autoUpdateSeries(enable = true)
332{
333 if (_data.autoUpdateSeries === enable)
334 return;
335
336 _data.autoUpdateSeries = enable;
337 updateSeries();
338 sendAutoUpdateSeriesState();
339 _data.instance.dataStore.autoUpdateSeries = _data.autoUpdateSeries;
340}
341
342// Toggles the automatic series updates. Convenience for `autoUpdateSeries()` based on current setting.
343export function autoUpdateSeriesToggle()
344{
345 autoUpdateSeries(!_data.autoUpdateSeries);
346}
347
348// Functions for setting and updating what happens when user presses a series color swatch.
349// This could select the swatch color as the current color, or copy the swatch color to clipboard.
350// Toggles between the two available modes. This is called in an Action triggered by a switch in the page.
351export function toggleSeriesColorPressMode()
352{
353 if (_data.seriesColorPressMode == "select")
354 _data.seriesColorPressMode = "copy";
355 else
356 _data.seriesColorPressMode = "select";
357 // Save current selection to persistent storage.
358 _data.instance.dataStore.seriesColorPressMode = _data.seriesColorPressMode;
359 // Update the corresponding State for page UI updates.
360 sendSeriesColorPressModeState();
361}
362
363// Internal color series helper functions
364
365// This internal function generates the last requested color series.
366function sendSeries()
367{
368 // Save the last used series to persistent storage object (this is saved/restored with the Script Instance when plugin exits/starts)
369 _data.instance.dataStore.lastSeries = _data.lastSeries;
370 switch(_data.lastSeries.t) {
371 case 'polyad':
372 sendColorsArray(color().polyad(_data.lastSeries.n+1).slice(1));
373 break;
374 case 'analogous':
375 sendColorsArray(color().analogous(_data.lastSeries.n+1, _data.lastSeries.s).slice(1));
376 break;
377 case 'monochromatic':
378 sendColorsArray(color().monochromatic(_data.lastSeries.n+1).slice(1));
379 break;
380 case '': {
381 let colors = [];
382 [...Array(_data.lastSeries.n).keys()].forEach(() => colors.push(COLOR_TRANS));
383 sendColorsArray(colors);
384 break;
385 }
386
387 default:
388 return;
389 }
390}
391
392// This internal function is used by the array generator functions above to send results back to Touch Portal as States.
393// The states are created dynamically as needed (in case they do not exist yet), and then updated to the current value from the array.
394// The state IDs and names are based on the current Dynamic Script Engine instance which is running this module.
395function sendColorsArray(arry, stateNamePrefix = "")
396{
397 // Comp. and split comp. arrays always have 1 or 2 members, respectively. When this function is called for either of those,
398 // the stateNamePrefix parameter is non-empty.
399 // The other color series can display up to _data.seriesMaxColors colors, which corresponds to the number
400 // of display "slots" on the color panel page design (6). If a color array has fewer than seriesMaxColors members
401 // then the rest get filled with transparent colors.
402 const maxIdx = arry.length - 1;
403 const maxColors = stateNamePrefix ? maxIdx + 1 : _data.seriesMaxColors;
404 for (let i=0; i < maxColors; ++i) {
405 // create the state ID
406 let stateId = _data.stateId + '_';
407 if (stateNamePrefix)
408 stateId += stateNamePrefix;
409 else
410 stateId += "series";
411 if (maxColors > 1)
412 stateId += `_${i+1}`;
413 // Create new state if needed
414 if (_data.colorSeriesStates.indexOf(stateId) < 0) {
415 let stateDescript = STATES_GROUP_NAME + " ";
416 if (stateNamePrefix)
417 stateDescript += stateNamePrefix;
418 else
419 stateDescript += "series color";
420 if (maxColors > 1)
421 stateDescript += ` ${i+1}`;
422 TP.stateCreate(stateId, STATES_GROUP_NAME, stateDescript, "#00000000");
423 _data.colorSeriesStates.push(stateId);
424 }
425 // Now send the actual color. But first, if the given array does not have a color for this array index,
426 // then fill it with a transparent color instead.
427 if (i > maxIdx)
428 arry.push(COLOR_TRANS);
429 TP.stateUpdateById(stateId, arry[i].tpcolor().toUpperCase());
430 }
431}
432
433// Internal function used to perform automatic updates of complement/split comp./series colors when current color changes.
434function updateSeries()
435{
436 if (!_data.autoUpdateSeries)
437 return;
438 complement();
439 splitcomplement();
440 sendSeries();
441}
442
443// Internal function to send the name of the currently selected color series as a State,
444// eg. for highlighting the corresponding button on the page.
445function sendCurrentSeriesNameState()
446{
447 var seriesName = '';
448 // Format the series name to look like the function call which invoked it (this way the parameters can also be matched);
449 // eg: "analogous(6, 18)"
450 if (_data.lastSeries.t) {
451 seriesName = `${_data.lastSeries.t}(${_data.lastSeries.n}`;
452 if (_data.lastSeries.s > 0)
453 seriesName += ", " + _data.lastSeries.s;
454 seriesName += ')';
455 }
456 TP.stateUpdateById(_data.lastSeriesNameState, seriesName);
457}
458
459// Send a State update with the new value of the autoUpdateSeries setting (eg. to use as button visual change trigger).
460function sendAutoUpdateSeriesState()
461{
462 TP.stateUpdateById(_data.autoUpdateEnabledState, _data.autoUpdateSeries ? "1" : "0");
463}
464
465// Send a State update with the current value of the seriesColorPressMode setting.
466function sendSeriesColorPressModeState()
467{
468 TP.stateUpdateById(_data.seriesColorPressModeState, _data.seriesColorPressMode);
469}
470
471
472// ------------------------------------------
473// Clipboard functions. Uses Clipboard example module, imported at top.
474// NOTE: Linux requires `xclip` utility installed.
475
476// Tries to set a new color from a clipboard value. Accepted formats are same as described in `setColor()` above.
477// In case the clipboard does not hold a valid color value, this function will log a warning message and return
478// the current color instead (no changes to current color will be made).
479export function fromClipboard()
480{
481 const val = clipboardText();
482 // console.info(LOG_PREFIX, "Clipboard value:", val);
483 const color = new Color(val);
484 if (color.isValid())
485 return setColor(color);
486 console.error(LOG_PREFIX, "Clipboard contents were not a valid color.");
487 return tpcolor();
488}
489
490// Starts monitoring the clipboard for a change. If a change is detected, it tries to paste
491// the clipboard contents, if any, as the current color by calling the `fromClipboard()` function above.
492// The timeout parameter controls how long to monitor the clipboard for changes, in seconds (default is 30).
493// The monitoring is cancelled after one change is detected (whether the value was a valid color or not),
494// or the timeout expires, whichever occurs first.
495export function monitorClipboard(timeout = 30)
496{
497 if (!_data.clipboardMonitorTimer) {
498 _data.clipboardMonitorTimer = setTimeout(checkClipboard, timeout * 1000, /* timeout: */ true);
499 clipboardChanged.connect(checkClipboard);
500 console.info(LOG_PREFIX, "Clipboard monitor started with " + timeout + " second timeout.");
501 }
502}
503
504function checkClipboard(timeout = false)
505{
506 if (_data.clipboardMonitorTimer) {
507 clearTimeout(_data.clipboardMonitorTimer);
508 _data.clipboardMonitorTimer = 0;
509 clipboardChanged.disconnect(checkClipboard);
510 if (!timeout && clipboardTextAvailable())
511 fromClipboard();
512 console.info(LOG_PREFIX, "Clipboard monitor " + (timeout ? "timed out." : "checked clipboard."));
513 }
514}
515
516// --------------------------------------
517// Miscellaneous exported/public "utility" functions.
518
519// Converts a "#AARRGGBB" color format string to "#RRGGBBAA" format. If input is less than 9 characters long then
520// no conversion is done and the original string is returned.
521export function argb2rgba(argbString)
522{
523 return argbString.length === 9 ? argbString.replace(/^#([0-9a-z]{2})([0-9a-z]{6})$/i, '#$2$1') : argbString;
524}
525
526// Returns a new Color instance from "#AARRGGBB" or "#RRGGBB" string input.
527export function colorFromArgb(argbString)
528{
529 return new Color(argb2rgba(argbString));
530}
531
532// Returns a color formatted as "#RRGGBB" or "#RRGGBBAA" depending on the current alpha channel value.
533// The alpha component will be included if it is not fully opaque (< 1.0).
534export function hexColor(color)
535{
536 const c = new Color(color);
537 if (c.alpha() < 1)
538 return c.rgba();
539 return c.hex();
540}
541
542// Takes a "#AARRGGBB" or "#RRGGBB" string input and
543// returns a color formatted as "#RRGGBB" or "#RRGGBBAA" depending on the current alpha channel value.
544export function hexFromArgb(argbString)
545{
546 return hexColor(colorFromArgb(argbString));
547}
548
549
550// --------- No further exported/public functions below -----------------
551
552// ------------------------------------------
553// Connector/slider feedback handling
554// These functions are all about updating the slider positions in the page to correspond to the currently selected color.
555// For example if the Hue is adjusted, the RGB sliders should be updated to reflect the current values, and vice versa.
556// Or if the step buttons are used to adjust color instead of the sliders, the sliders should still reflect the current position (as closely as possible).
557
558// This function queries the global DSE connector tracking database to try to find the "short ID" of the connector
559// we're interested in. We can do this based on some criteria; in this case we can use the current instance (State) Name,
560// (which was read and saved in `init()``) and also the expression being used. For example the red channel
561// slider uses the expression "M.setRed(${connector_value})". So if we search for "*setRed*" (using wildcards), this should give us the
562// slider for the red channel.
563// We also cache (save) the short ID lookups, because they are not going to change very often, if ever, and searching for
564// them every time a slider is moved is a waste. See the `init()` function at the bottom for how the cache is cleared if connectors are changed on the page.
565function getShortId(channel)
566{
567 if (!_data.connectorIds[channel] && _data.instance) {
568 const shortIds = TP.getConnectorShortIds({ instanceName: _data.instance.name, expression: `*set${channel}*` });
569 if (shortIds.length)
570 _data.connectorIds[channel] = shortIds[0];
571 //console.log(channel, _data.connectorIds[channel]);
572 }
573 return _data.connectorIds[channel];
574}
575
576// Generic function to update any of the color channel sliders by name with given value.
577function updateConnector(channel, value)
578{
579 const shortId = getShortId(channel);
580 if (shortId)
581 TP.connectorUpdateShort(shortId, value.clamp(0, 100));
582}
583
584// These functions are basically just conveniences to set each slider individually, instead of repeating the same code in several of places.
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))); }
593
594// This function sets all the RGB sliders, eg. when any of the HSV controls change.
595function updateRgbConnectors()
596{
597 const rgb = color().toRgb();
598 updateRedConnector(rgb.r);
599 updateGreenConnector(rgb.g);
600 updateBlueConnector(rgb.b);
601}
602
603// This function sets all the HSV slider, eg. when any of the RGB channels change.
604function updateHsvConnectors()
605{
606 const hsv = color().toHsv();
607 updateHueConnector(hsv.h);
608 updateSatConnector(hsv.s);
609 updateValueConnector(hsv.v);
610}
611
612// ------------------------------------------
613// Other internal utility functions/data.
614
615// This function runs each time the current color is changed/updated. It runs any related updaters as needed
616// (eg. the automatic text color and series generators), potentially updates connectors, and most
617// importantly it sends the current color to TP, both as an `#AARRGGBB` value for displaying on the swatch,
618// and as a formatted string (using the FORMATTED_COLOR_TEMPLATE template defined at the top)
619// with the current color values for displaying on top of the swatch (or wherever).
620// connectors to update: 0 = none; 1 = rgb; 2 = hsv; 3 = rgb + hsv + alpha
621function update(connectors = 0)
622{
623 TP.stateUpdateById(_data.stateId, tpcolor());
624 updateTextColor();
625 updateSeries();
626
627 // Update the saved color value in persistent data. Next time the color panel instance is loaded from settings, the last used color will be restored.
628 _data.instance.dataStore.color = _data.color.rgba();
629
630 // Send a State update with the new color as formatted text.
631 const rgb = _data.color.toRgb();
632 const hsv = _data.color.toHsv();
633 const lum = _data.color.luminance();
634 //const hsl = _data.color.toHsl();
635 const value = Format(
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)
639 );
640 TP.stateUpdateById(_data.formattedColorState, value);
641 //console.log(lum, hsl.l, hsv.h, hsl.h, hsv.s, hsl.s);
642
643 // loop over all the color channel values and update States as needed.
644 for (const [k, v] of Object.entries(_data.channelValues)) {
645 if (!v.state)
646 continue;
647 const newVal = v.get(rgb, hsv, lum);
648 if (newVal != v.v) {
649 v.v = newVal;
650 TP.stateUpdateById(v.state, Format(v.fmt, v.v));
651 }
652 }
653
654 if (connectors & 1)
655 updateRgbConnectors();
656 if (connectors & 2)
657 updateHsvConnectors();
658 if (connectors == 3)
659 updateAlphaConnector(rgb.a);
660
661 //return tpcolor();
662 return undefined;
663}
664
665function updateRepeatRate(ms) {
666 TP.stateUpdateById(_data.repeatRateState, ms.toString());
667}
668function updateRepeatDelay(ms) {
669 TP.stateUpdateById(_data.repeatDelayState, ms.toString());
670}
671
672// Do first-run initialization tasks.
673function init()
674{
675 if (_data.init)
676 return;
677 console.info(LOG_PREFIX, "initializing...");
678
679 // Get the current script instance object (`DynamicScript` type);
680 _data.instance = DSE.currentInstance();
681 if (!_data.instance) {
682 console.error("Something went wrong, could not find current DynamicScript instance!");
683 return;
684 }
685 _data.init = true;
686 _data.stateId = _data.instance.stateId;
687 // Set the category and name with which the primary State will be created.
688 _data.instance.stateParentCategory = STATES_GROUP_NAME;
689
690 // Get the stored persistent data object
691 const ds = _data.instance.dataStore;
692 // Restore persistently stored data, if any, otherwise create it.
693 if (ds.color) {
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;
702 }
703 else {
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;
711 }
712
713 // Create a State for primary color swatch display, in #AARRGGBB format (used by TP for setting button background color).
714 _data.formattedColorState = _data.stateId + '_formatted';
715 let stateDescript = STATES_GROUP_NAME + " - Current Color";
716 // ID, Category, Description, Default
717 TP.stateCreate(_data.stateId, STATES_GROUP_NAME, stateDescript, "");
718
719 // Create a State for primary color as formatted text.
720 _data.formattedColorState = _data.stateId + '_formatted';
721 stateDescript = STATES_GROUP_NAME + " Formatted Color";
722 TP.stateCreate(_data.formattedColorState, STATES_GROUP_NAME, stateDescript, "");
723
724 // Loop over all the color channel values and create States.
725 for (const [k, v] of Object.entries(_data.channelValues)) {
726 // Create a state with a friendly name for this channel.
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, "");
730 }
731
732 // Create a State for the current text color value.
733 _data.textColorState = _data.stateId + '_textColor';
734 stateDescript = STATES_GROUP_NAME + " Text Color";
735 // the default should be the actual text color, but due to a "bug" in TP <= v3.1 we need to force it to a "wrong" value first.
736 TP.stateCreate(_data.textColorState, STATES_GROUP_NAME, stateDescript, _data.textColor.lighten().rgba());
737
738 // Create a State for the current series color swatch press mode
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, "");
742
743 // Create the State current color series name.
744 _data.lastSeriesNameState = _data.stateId + '_selectedSeries';
745 stateDescript = STATES_GROUP_NAME + " Selected Color Series";
746 // the default should be 0, but due to a "bug" in TP <= v3.1 we need to force it to a blank value first.
747 TP.stateCreate(_data.lastSeriesNameState, STATES_GROUP_NAME, stateDescript, "");
748
749 // Create a State for the autoUpdateSeries flag.
750 _data.autoUpdateEnabledState = _data.stateId + '_autoUpdateSeries';
751 stateDescript = STATES_GROUP_NAME + " Auto Update Complement/Series";
752 // the default should be 0, but due to a "bug" in TP <= v3.1 we need to force it to a blank value first.
753 TP.stateCreate(_data.autoUpdateEnabledState, STATES_GROUP_NAME, stateDescript, "");
754
755 // Create States for the current action repeat rate and delay.
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");
762
763 // Connect to the `DynamicScript.repeat*Changed` events to send repeat rate/delay state updates.
764 _data.instance.repeatRateChanged.connect(updateRepeatRate);
765 _data.instance.repeatDelayChanged.connect(updateRepeatDelay);
766
767 // Register an event handler with the global connectors database to get notified
768 // when connector data has been updated (eg. a slider has been added/removed/changed).
769 // For our needs it's simpler to just clear the cache when this happens, and it will
770 // be re-populated automatically as needed (in `getShortId()`);
771 // The `connectorIdsChanged()` event has one parameter, which is the name of the instance/State
772 // for which the changed connector(s) are used (this is in their State Name field). So
773 // we only need to react to changes made to "our" connectors, where the instanceName matches the
774 // current one in `_data.instance.name`.
775 TP.onconnectorIdsChanged( (instanceName) => {
776 if (_data.instance && instanceName == _data.instance.name) {
777 _data.connectorIds = {};
778 console.info(LOG_PREFIX, "Connector ID cache cleared.");
779 }
780 });
781
782 // Send state updates now to trigger TP events and update mixer page visuals.
783 update(3);
784 sendCurrentSeriesNameState();
785 sendAutoUpdateSeriesState();
786 sendSeriesColorPressModeState();
787 updateRepeatRate(_data.instance.effectiveRepeatRate);
788 updateRepeatDelay(_data.instance.effectiveRepeatDelay);
789
790 console.info(LOG_PREFIX, "initialization completed.");
791}
792
793// Misc. contstants.
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:";
798
799// Internal temporary data storage. _data is undefined at startup and after an Engine reset.
800var _data = _data ||
801{
802 init: false, // initialization flag, set to true after first time `init()` is called.
803 color: COLOR_WHITE.clone(), // the current color
804 textColor: COLOR_BLACK.clone(), // the current text color (eg. to show on color swatch)
805 instance: null, // the ColorMixer primary DynamicScript instance, set in init()
806 stateId: "", // State ID for the current color value; set in init()
807 textColorAuto: true, // whether to automatically set the text color based on overall darkness of current main color
808 seriesColorPressMode: 'select', // stores the desired behavior when user presses one of the color series tiles: 'select' or 'copy'
809 autoUpdateSeries: false, // enable/disable automatic update of complement/split comp./last series when current color changes
810 lastSeries: { t: "", n: 0, s: 0 }, // track which series type was last selected (if any)
811 seriesMaxColors: 6, // maximum number of colors in a series; should match the page layout
812 colorSeriesStates: [], // keep track of created state IDs for color series so we do not re-create them each time
813 formattedColorState: "", // ID of created formatted color state
814 textColorState: "", // ID of created text color state
815 seriesColorPressModeState: "", // ID of created state for seriesColorPressMode value
816 lastSeriesNameState: "", // ID of created state for the last selected color series name
817 autoUpdateEnabledState: "", // ID of created series auto-update status state
818 repeatRateState: "", // ID of created action repeat rate value state
819 repeatDelayState: "", // ID of created action repeat delay value state
820 connectorIds: {}, // track connector `shortId`s which are used in the page to set color values; these are used for updating slider positions.
821 // track values and states of color channels (to show under sliders, for example);
822 // track the values so that we do not send needless updates when they do not actually change; also makes it possible to reference the current channel value at any time
823 channelValues: {
824 //C: Name for State, Value, State ID, Get value function, Formatting string (.NET style for `String.format()`)
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} %" },
833 /*
834 // For example to display the RGB values in 0-256 range instead of percent, change the result value and the format string:
835 r: { name: "Red", v: -1, state: "", get: (rgb, _, __) => rgb.r, fmt: "{0:000}" },
836 g: { name: "Green", v: -1, state: "", get: (rgb, _, __) => rgb.g, fmt: "{0:000}" },
837 b: { name: "Blue", v: -1, state: "", get: (rgb, _, __) => rgb.b, fmt: "{0:000}" },
838 */
839 },
840};
841
842// Conversion utilities
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; }
847
848// Adds `d`[elta] to `v`[alue] and returns integer in 0-255 range.
849function wrapShort(v, d) {
850 var p = (v + d.clamp(-255, 255)) % 256;
851 return (p < 0 ? 256 + p : p);
852}
853
854// Adds `d`[elta] to `v`[alue] and returns integer in 0-359 range.
855function wrapDegrees(v, d) {
856 var p = (v + d.clamp(-360, 360)) % 360;
857 return (p < 0 ? 360 + p : p);
858}
859
860// Adds (`d`[elta] / 100) to `v`[alue] and returns decimal in 0-1 range.
861function wrapPercent(v, d) {
862 var p = v + clamp(d * 0.01, -1, 1);
863 return p - floor(p);
864}
865
866// Initialize on first load (or import, since this is a module).
867if (!_data.init)
868 init();
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,...