123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- <?php
- /* wrapper for curl, sends a single parameter to the esp8266, no attempt is made to interpret
- * user data, except that we protect curl from possible injection. Because protection is heavy
- * handed, end-user data can easily be mangled, as in, if it isn't formatted right it is going to
- * be forced through a correctly formatted die
- */
- function curl($postParam, $len, $route, $varName = "var") {
- $uri = "http://rain-gutter-rgb.lan.rome7.com/{$route}";
- $unsafeUserData = "";
- // start with an empty, off, or "zeroed" buffer
- for($i = $len; $i--;) $unsafeUserData .= "0";
- // [!] protect curl from injection, only allow hexidecimal digits 0..9a..fA..F]
- for($i = $len; $i--; ) $unsafeUserData[$i] = (isset($_POST[$postParam][$i]) && ctype_xdigit($_POST[$postParam][$i]))
- ? $_POST[$postParam][$i]
- : '0';
- $ch = curl_init();
- error_log("sending [{$postParam}] '{$unsafeUserData}' to {$uri}");
- curl_setopt($ch, CURLOPT_URL, $uri);
- curl_setopt($ch, CURLOPT_POST, 1);
- curl_setopt($ch, CURLOPT_POSTFIELDS, "{$postParam}={$unsafeUserData}");
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- $out = curl_exec($ch);
- curl_close($ch);
- return $out;
- }
- /* check for a POST request and use curl to send data to the esp8266
- * default response is http bad request
- * passthru contains a simple description of the esp8266 api
- */
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $code = 400; $out = "";
- $passthru = array(
- // POST_VARIABLE => [buffer-length, api-endpoint]
- "color" => array(6, "rgb"),
- "brightness" => array(2, "brightness"),
- "quickGamma" => array(1, "cfg"),
- "sCurveGamma" => array(1, "cfg"),
- "colorCorrection" => array(1, "cfg")
- );
- // execute the first matching command
- foreach($passthru as $k => $v) {
- if(!isset($_POST[$k])) continue; $out = curl($k, $v[0], $v[1]); $code = 200; break; }
- // set headers
- header("HTTP/1.0 " . strval($code) . (200 === $code ?" OK" : " Bad Request"));
- $GLOBALS["http_response_code"] = $code;
- //http_response_code($code);
- exit("{$out}");
- } /* else GET request, check for quick response parameters -- do not generate whole page */ {
- if(isset($_GET['req'])) {
- // default is to get color as decimal [0..255] values R,G,B
- $req = "dec";
- switch($_GET['req']) {
- case "color": $req = "dec"; break;
- case "brightness": $req = "brightness"; break;
- case "correction": $req = "cfg"; break;
- }
- $uri = "http://rain-gutter-rgb.lan.rome7.com/{$req}";
- // send GET request to the esp8266
- $ch = curl_init();
- error_log("sending [get: {$req}] via {$uri}");
- curl_setopt($ch, CURLOPT_URL, $uri);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- $out = curl_exec($ch);
- curl_close($ch);
- exit($out);
- }
- }
- // otherwise - generate a whole web-page
- echo "<html>";
- echo "<body>";
- // either use mixbox non-commercial use under the CC BY-NC 4.0 license
- echo "<script src='mixbox.js'></script>";
- // or the spectral.js MIT license
- echo "<script src='spectral.js'></script>";
- echo "
- <script>
- // example mixing two colors
- var rgb1 = 'rgb(0, 33, 133)'; // blue
- var rgb2 = 'rgb(252, 211, 0)'; // yellow
- var t = 0.5; // mixing ratio
- var mixed = mixbox.lerp(rgb1, rgb2, t);
- console.log(mixed);
- // example mixing multiple colors
- var z1 = mixbox.rgbToLatent(rgb1);
- var z2 = mixbox.rgbToLatent(rgb2);
- var z3 = mixbox.rgbToLatent(mixed);
- var zMix = new Array(mixbox.LATENT_SIZE);
- for (var i = 0; i < zMix.length; i++) { // mix:
- zMix[i] = (0.3*z1[i] + // 30% of rgb1
- 0.6*z2[i] + // 60% of rgb2
- 0.1*z3[i]); // 10% of mixed
- }
- var rgbMix = mixbox.latentToRgb(zMix);
- console.log(rgbMix);
- // using spectral.js to mix two colors (does not seem to be working..., returns [0,0,0])
- var color = spectral.mix('[0, 33, 133]', '[252, 211, 0]', 0.5, spectral.RGB);
- console.log(color);
- </script>
- ";
- // common functions
- echo "
- <script>
- // using the newer fetch API, not sure if this is worse than AJAX
- async function post(url = '', kw = 'key', data = 'data') {
- // to use _POST superblogal, send body as http multipart/form-data
- let fd = new FormData();
- fd.append(kw, data);
- return await fetch(url, { method: 'POST', body: fd });
- }
- /* I assume http get requests may use default
- * [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
- * parameters.
- */
- async function get(arg) { return await fetch('?' + new URLSearchParams({req:arg})).then((v) => v.text()); }
- function getColor(selectedObj = null) {
- // get the color from the esp8266, returns as decimal [0..255] R,G,B
- let dec = get('color');
- // split into red, green, and blue components:
- let rgb = dec.then((v) => { let a = v.split(',').map((i) => parseInt(i, 10)); return {red: a[0], green: a[1], blue: a[2]}; });
- rgb.then((v) => console.log('esp8266 color: ', v));
- // by default, return as Promise that will resolve to rgb object { red: x, green: y, blue: z }
- if(null === selectedObj) return rgb;
- // otherwise set backround and text of the selected color object
- rgb.then((v) => {
- let a = [v.red, v.green, v.blue];
- color = 'rgb(' + a.join(', ') + ')';
- let off = a.every((x) => 0 === x);
- // use gray as a placeholder for 'lights off'
- selectedObj.style.background = off ?'rgb(211, 211, 211)' :color;
- selectedObj.textContent = color;
- });
- }
- function getBrightness(sliderObj = null) {
- // get the brightness from the esp8266, returns as decimal [0..15]
- let brightness = get('brightness').then((v) => parseInt(v,10));
- brightness.then((i) => { console.log('esp8266 brightness: ', i); });
- // by default, return as Promise that will resolve to integer
- if(null === sliderObj) return brightness;
- // otherwise set slider object's value property to brightness when the promise resolves
- brightness.then((i) => { sliderObj.value = i; });
- }
- function getCorrection(inputs = null) {
- /* get the correction config from the esp8266, returns as cfg [ setting:[0..1], ... setting_n:[0..1]]
- * settings currently include:
- * quickGamma - corrects for human perception using quadratic function
- * sCurveGamma - corrects for human perception using sigmoid function
- * colorCorrection - LED manufacturer correction, differences between doping used for different colors
- *
- * + quickGamma and sCurveGamma can be turned on or off and are are mutually
- * exclusive to eachother
- * + colorCorrection may be turned on or off, and can be applied to either
- * gamma correction schemes
- */
- let cfg = get('correction');
- // split into config options
- let options = cfg.then((v) => {
- let temp = {};
- // convert to lower-case and keep only alpha-numeric
- v = v.toLowerCase().replace(/[^a-z0-9]/gi, '');
- temp.quickGamma = '0' !== v.split('quickgamma')[1][0];
- temp.sCurveGamma = '0' !== v.split('scurvegamma')[1][0];
- temp.colorCorrection = '0' !== v.split('colorcorrection')[1][0];
- // quick and sCurve are mutually exclusive, though they can both be off
- temp.noneGamma = temp.quickGamma === temp.sCurveGamma;
- return temp;
- });
- options.then((o) => console.log('esp8266 settings: ', o));
- // by default, return as Promise that will resolve to config options object
- if(null === inputs) return options;
- // otherwise set options
- for(let i = 0; i < inputs.length; i++) options.then((o) => {
- inputs[i].checked = Object.hasOwn(o, inputs[i].id) ?o[inputs[i].id] :false;
- });
- }
- </script>
- ";
- // render a color selection wheel
- echo "
- <script>
- var canvas = document.createElement('canvas');
- // google chrome performance flag
- //var context = canvas.getContext('2d');
- var context = canvas.getContext('2d', { willReadFrequently: true });
- var w = 400; var h = 400;
- canvas.width = w;
- canvas.height = h;
- for(var i = 0; i < 360; i += 0.1) {
- var rad = i * (2 * Math.PI) / 360;
- var x1 = w/2 + w/2 * Math.cos(rad);
- var y1 = h/2 + h/2 * Math.sin(rad);
- var gradient = context.createLinearGradient(w/2, h/2, x1, y1);
- gradient.addColorStop(0.0, 'white');
- gradient.addColorStop(1.0, 'hsla('+i+', 100%, 50%, 1.0)');
- context.strokeStyle = gradient;
- context.beginPath();
- context.moveTo(w/2, h/2);
- context.lineTo(x1, y1);
- context.stroke();
- }
- document.body.appendChild(canvas);
- </script>
- ";
- // simple color picker
- echo "
- <script>
- function tableData(c, head = false) {
- let td = document.createElement(head ?'th' :'td');
- if(typeof c === 'string' || c instanceof String) td.innerText = c;
- else td.appendChild(c);
- return td;
- }
- // create a table
- var table = document.createElement('table');
- var tableHead = document.createElement('thead');
- table.appendChild(tableHead);
- var tableHeadRow = document.createElement('tr');
- tableHead.appendChild(tableHeadRow);
- var tableBody = document.createElement('tbody');
- table.appendChild(tableBody);
- var tableBodyRow = document.createElement('tr');
- tableBody.appendChild(tableBodyRow);
- // add some headers
- tableHeadRow.appendChild(tableData('', true));
- tableHeadRow.appendChild(tableData('hover', true));
- tableHeadRow.appendChild(tableData('selected', true));
- // add table data
- tableBodyRow.appendChild(tableData(canvas));
- hovered = tableData(''); hovered.id = 'hovered-color';
- tableBodyRow.appendChild(hovered);
- selected = tableData(''); selected.id = 'selected-color';
- tableBodyRow.appendChild(selected);
- document.body.appendChild(table);
- // set the inital value of the selected color cell to the esp8266 lights
- getColor(selected);
- // listen to events from canvas
- function pick(event, destination, cb = false) {
- const bounding = canvas.getBoundingClientRect();
- const x = event.clientX - bounding.left;
- const y = event.clientY - bounding.top;
- const pixel = context.getImageData(x, y, 1, 1);
- const data = pixel.data;
- //console.log(pixel, data);
- const rgb = 'rgb(' + data[0] + ', ' + data[1] + ', ' + data[2] + ')';
- let off = 0 === (data[0] | data[1] | data[2]);
- // use 211,211,211 (gray) as a placeholder for 'lights turned off'
- destination.style.background = off ?'rgb(211, 211, 211)' :rgb;
- destination.textContent = rgb;
- // handle optional callback
- if(false !== cb) cb(data[0], data[1], data[2]);
- return rgb;
- }
- canvas.addEventListener('mousemove', (event) => pick(event, hovered));
- canvas.addEventListener('click', (event) => pick(event, selected));
- </script>
- ";
- // add a callback to the click event to send rgb value to esp8266
- echo "
- <script>
- function sg(x) { return Math.round((x*x)/255); }
- function callback(red, green, blue) {
- console.log('logged', red, green, blue);
- post(
- /* url */ '',
- /* key */ 'color',
- sg(red).toString(16).padStart(2,'0') +
- sg(green).toString(16).padStart(2,'0') +
- sg(blue).toString(16).padStart(2,'0')
- ).then((data) => { console.log(data, data.text()); });
- }
- canvas.removeEventListener('click', (event) => pick(event, selected));
- canvas.addEventListener('click', (event) => pick(event, selected, callback));
- </script>
- ";
- /* make a brightness slider, esp8266 takes 0..15 levels of brightness
- * zero is off and should be ignored
- *
- * currently brightness control is very simple and operates without
- * modifying end-user color choices
- *
- * tldr; users have 24bit color selectivity, in hardware we have a
- * little over 1.5 bytes per color giving us somewhere near 37bit color
- * selectivity. so a user can select 0a3bcc as their color choise and
- * we can have thousands of shades of colors that are close to that.
- * The power section of the esp8266 is not optimal so the esp8266 will
- * limit running the LEDs at maximum current. This limits the color-space
- * by about 20%. The way we implement brightness is to cut give the end
- * user 24-bits of color selectivity, that fits into 1/20th of the color-space
- * then we can multiply those values linearly 1..15 before we hit that 80%
- * power limit. because we are amplifying colors evenly the selected output
- * color will not change.
- */
- echo "
- <script>
- var slider = document.createElement('input');
- slider.type = 'range';
- slider.min = 1;
- slider.max = 15;
- slider.value = 7;
- slider.step = 1;
- var brightnessText = document.createElement('div');
- brightnessText.innerHTML = 'brightness';
- document.body.appendChild(brightnessText);
- document.body.appendChild(slider);
- // set the inital value of the slider to whatever the current brightness value
- getBrightness(slider);
- function slid(event, cb = false) {
- if(false !== cb) cb(slider.value);
- console.log('changed slider to ' + slider.value);
- return 'brightness(' + slider.value + ')';
- }
- slider.addEventListener('change', (event) => slid(event));
- </script>
- ";
- // add a callback to the off-click event on the slider
- echo "
- <script>
- function slidercb(valu) {
- console.log('logged', valu);
- post('', 'brightness', parseInt(valu, 10).toString(16).padStart(2,'0')).then((data) => { console.log(data, data.text()); });
- }
- slider.removeEventListener('change', (event) => slid(event));
- slider.addEventListener('change', (event) => slid(event, slidercb));
- </script>
- ";
- // add a checkbox and radio set to select config options for color correction
- echo "
- <script>
- function addChoice(inputs, parent, name, value, alternate = null, type = null) {
- var inp = document.createElement('input');
- inp.type = null === type ?'radio' :type;
- let f = value.replace(/[^a-zA-Z]/gi, '') + name.charAt(0).toUpperCase() + name.slice(1);
- inp.id = f;
- inp.name = name;
- inp.value = f;
- var lab = document.createElement('label');
- lab.for = f;
- lab.innerHTML = null === alternate ?value :alternate;
- parent.appendChild(inp);
- parent.appendChild(lab);
- inputs.push(inp);
- }
- function configChange(e) {
- /* check for noneGamma special radio button, unfortunately server does not
- * have an option to turn off both sCurveGamma and quickGamma, but we
- * want to provide this to end users...
- */
- if('noneGamma' === e.target.id) {
- post('', 'quickGamma', '0').then((d) => console.log(d.text()));
- post('', 'sCurveGamma', '0').then((d) => console.log(d.text()));
- } else {
- post('', e.target.id, e.target.checked ?'1' :'0').then((d) => console.log(d.text()));
- }
- //console.log(e);
- }
- var gamma = document.createElement('fieldest');
- var legend = document.createElement('legend');
- gamma.appendChild(legend);
- let inputs = [];
- addChoice(inputs, gamma, 'gamma', 'none');
- addChoice(inputs, gamma, 'gamma', 'sCurve');
- addChoice(inputs, gamma, 'gamma', 'quick');
- addChoice(inputs, gamma, 'correction', 'color', 'led-bias', 'checkbox');
- legend.innerHTML = 'color correction options';
- document.body.appendChild(gamma);
- // set config options from the esp8266
- getCorrection(inputs);
- // add listeners to each color correction config option
- for(let i = 0; i < inputs.length; i++) {
- inputs[i].addEventListener('change', (event) => configChange(event));
- }
- </script>
- ";
- echo "</body>";
- echo "</html>";
|