index.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. /* wrapper for curl, sends a single parameter to the esp8266, no attempt is made to interpret
  3. * user data, except that we protect curl from possible injection. Because protection is heavy
  4. * handed, end-user data can easily be mangled, as in, if it isn't formatted right it is going to
  5. * be forced through a correctly formatted die
  6. */
  7. function curl($postParam, $len, $route, $varName = "var") {
  8. $uri = "http://rain-gutter-rgb.lan.rome7.com/{$route}";
  9. $unsafeUserData = "";
  10. // start with an empty, off, or "zeroed" buffer
  11. for($i = $len; $i--;) $unsafeUserData .= "0";
  12. // [!] protect curl from injection, only allow hexidecimal digits 0..9a..fA..F]
  13. for($i = $len; $i--; ) $unsafeUserData[$i] = (isset($_POST[$postParam][$i]) && ctype_xdigit($_POST[$postParam][$i]))
  14. ? $_POST[$postParam][$i]
  15. : '0';
  16. $ch = curl_init();
  17. error_log("sending [{$postParam}] '{$unsafeUserData}' to {$uri}");
  18. curl_setopt($ch, CURLOPT_URL, $uri);
  19. curl_setopt($ch, CURLOPT_POST, 1);
  20. curl_setopt($ch, CURLOPT_POSTFIELDS, "{$postParam}={$unsafeUserData}");
  21. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  22. $out = curl_exec($ch);
  23. curl_close($ch);
  24. return $out;
  25. }
  26. /* check for a POST request and use curl to send data to the esp8266
  27. * default response is http bad request
  28. * passthru contains a simple description of the esp8266 api
  29. */
  30. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  31. $code = 400; $out = "";
  32. $passthru = array(
  33. // POST_VARIABLE => [buffer-length, api-endpoint]
  34. "color" => array(6, "rgb"),
  35. "brightness" => array(2, "brightness")
  36. );
  37. // execute the first matching command
  38. foreach($passthru as $k => $v) {
  39. if(!isset($_POST[$k])) continue; $out = curl($k, $v[0], $v[1]); $code = 200; break; }
  40. // set headers
  41. header("HTTP/1.0 " . strval($code) . (200 === $code ?" OK" : " Bad Request"));
  42. $GLOBALS["http_response_code"] = $code;
  43. //http_response_code($code);
  44. exit("{$out}");
  45. } /* else GET request, check for quick response parameters -- do not generate whole page */ {
  46. if(isset($_GET['req'])) {
  47. // default is to get color as decimal [0..255] values R,G,B
  48. $req = "dec";
  49. switch($_GET['req']) {
  50. case "color": $req = "dec"; break;
  51. case "brightness": $req = "brightness"; break;
  52. case "correction": $req = "cfg"; break;
  53. }
  54. $uri = "http://rain-gutter-rgb.lan.rome7.com/{$req}";
  55. // send GET request to the esp8266
  56. $ch = curl_init();
  57. error_log("sending [get: {$req}] via {$uri}");
  58. curl_setopt($ch, CURLOPT_URL, $uri);
  59. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  60. $out = curl_exec($ch);
  61. curl_close($ch);
  62. exit($out);
  63. }
  64. }
  65. // otherwise - generate a whole web-page
  66. echo "<html>";
  67. echo "<body>";
  68. // either use mixbox non-commercial use under the CC BY-NC 4.0 license
  69. echo "<script src='mixbox.js'></script>";
  70. // or the spectral.js MIT license
  71. echo "<script src='spectral.js'></script>";
  72. echo "
  73. <script>
  74. // example mixing two colors
  75. var rgb1 = 'rgb(0, 33, 133)'; // blue
  76. var rgb2 = 'rgb(252, 211, 0)'; // yellow
  77. var t = 0.5; // mixing ratio
  78. var mixed = mixbox.lerp(rgb1, rgb2, t);
  79. console.log(mixed);
  80. // example mixing multiple colors
  81. var z1 = mixbox.rgbToLatent(rgb1);
  82. var z2 = mixbox.rgbToLatent(rgb2);
  83. var z3 = mixbox.rgbToLatent(mixed);
  84. var zMix = new Array(mixbox.LATENT_SIZE);
  85. for (var i = 0; i < zMix.length; i++) { // mix:
  86. zMix[i] = (0.3*z1[i] + // 30% of rgb1
  87. 0.6*z2[i] + // 60% of rgb2
  88. 0.1*z3[i]); // 10% of mixed
  89. }
  90. var rgbMix = mixbox.latentToRgb(zMix);
  91. console.log(rgbMix);
  92. // using spectral.js to mix two colors (does not seem to be working..., returns [0,0,0])
  93. var color = spectral.mix('[0, 33, 133]', '[252, 211, 0]', 0.5, spectral.RGB);
  94. console.log(color);
  95. </script>
  96. ";
  97. // common functions
  98. echo "
  99. <script>
  100. // using the newer fetch API, not sure if this is worse than AJAX
  101. async function post(url = '', kw = 'key', data = 'data') {
  102. // to use _POST superblogal, send body as http multipart/form-data
  103. let fd = new FormData();
  104. fd.append(kw, data);
  105. return await fetch(url, { method: 'POST', body: fd });
  106. }
  107. /* I assume http get requests may use default
  108. * [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
  109. * parameters.
  110. */
  111. function get(arg) { return fetch('?' + new URLSearchParams({req:arg})).then((v) => v.text()); }
  112. function getColor(selectedObj = null) {
  113. // get the color from the esp8266, returns as decimal [0..255] R,G,B
  114. let dec = get('color');
  115. // split into red, green, and blue components:
  116. let rgb = dec.then((v) => { let a = v.split(',').map((i) => parseInt(i, 10)); return {red: a[0], green: a[1], blue: a[2]}; });
  117. rgb.then((v) => console.log('esp8266 color: ', v));
  118. // by default, return as Promise that will resolve to rgb object { red: x, green: y, blue: z }
  119. if(null === selectedObj) return rgb;
  120. // otherwise set backround and text of the selected color object
  121. rgb.then((v) => {
  122. let a = [v.red, v.green, v.blue];
  123. color = 'rgb(' + a.join(',') + ')';
  124. let off = a.every((x) => 0 === x);
  125. // use gray as a placeholder for 'lights off'
  126. selectedObj.style.background = off ?'rgb(211, 211, 211)' :color;
  127. selectedObj.textContent = color;
  128. });
  129. }
  130. function getBrightness(sliderObj = null) {
  131. // get the brightness from the esp8266, returns as decimal [0..15]
  132. let brightness = get('brightness').then((v) => parseInt(v,10));
  133. brightness.then((i) => { console.log('esp8266 brightness: ', i); });
  134. // by default, return as Promise that will resolve to integer
  135. if(null === sliderObj) return brightness;
  136. // otherwise set slider object's value property to brightness when the promise resolves
  137. brightness.then((i) => { sliderObj.value = i; });
  138. }
  139. function getCorrection() {
  140. /* get the correction config from the esp8266, returns as cfg [ setting:[0..1], ... setting_n:[0..1]]
  141. * settings currently include:
  142. * quickGamma - corrects for human perception using quadratic function
  143. * sCurveGamma - corrects for human perception using sigmoid function
  144. * colorCorrection - LED manufacturer correction, differences between doping used for different colors
  145. *
  146. * + quickGamma and sCurveGamma can be turned on or off and are are mutually
  147. * exclusive to eachother
  148. * + colorCorrection may be turned on or off, and can be applied to either
  149. * gamma correction schemes
  150. */
  151. var cfg = get('correction');
  152. return cfg;
  153. }
  154. </script>
  155. ";
  156. // render a color selection wheel
  157. echo "
  158. <script>
  159. var canvas = document.createElement('canvas');
  160. // google chrome performance flag
  161. //var context = canvas.getContext('2d');
  162. var context = canvas.getContext('2d', { willReadFrequently: true });
  163. var w = 400; var h = 400;
  164. canvas.width = w;
  165. canvas.height = h;
  166. for(var i = 0; i < 360; i += 0.1) {
  167. var rad = i * (2 * Math.PI) / 360;
  168. var x1 = w/2 + w/2 * Math.cos(rad);
  169. var y1 = h/2 + h/2 * Math.sin(rad);
  170. var gradient = context.createLinearGradient(w/2, h/2, x1, y1);
  171. gradient.addColorStop(0.0, 'white');
  172. gradient.addColorStop(1.0, 'hsla('+i+', 100%, 50%, 1.0)');
  173. context.strokeStyle = gradient;
  174. context.beginPath();
  175. context.moveTo(w/2, h/2);
  176. context.lineTo(x1, y1);
  177. context.stroke();
  178. }
  179. document.body.appendChild(canvas);
  180. </script>
  181. ";
  182. // simple color picker
  183. echo "
  184. <script>
  185. function tableData(c, head = false) {
  186. let td = document.createElement(head ?'th' :'td');
  187. if(typeof c === 'string' || c instanceof String) td.innerText = c;
  188. else td.appendChild(c);
  189. return td;
  190. }
  191. // create a table
  192. var table = document.createElement('table');
  193. var tableHead = document.createElement('thead');
  194. table.appendChild(tableHead);
  195. var tableHeadRow = document.createElement('tr');
  196. tableHead.appendChild(tableHeadRow);
  197. var tableBody = document.createElement('tbody');
  198. table.appendChild(tableBody);
  199. var tableBodyRow = document.createElement('tr');
  200. tableBody.appendChild(tableBodyRow);
  201. // add some headers
  202. tableHeadRow.appendChild(tableData('', true));
  203. tableHeadRow.appendChild(tableData('hover', true));
  204. tableHeadRow.appendChild(tableData('selected', true));
  205. // add table data
  206. tableBodyRow.appendChild(tableData(canvas));
  207. hovered = tableData(''); hovered.id = 'hovered-color';
  208. tableBodyRow.appendChild(hovered);
  209. selected = tableData(''); selected.id = 'selected-color';
  210. tableBodyRow.appendChild(selected);
  211. document.body.appendChild(table);
  212. // set the inital value of the selected color cell to the esp8266 lights
  213. getColor(selected);
  214. // listen to events from canvas
  215. function pick(event, destination, cb = false) {
  216. const bounding = canvas.getBoundingClientRect();
  217. const x = event.clientX - bounding.left;
  218. const y = event.clientY - bounding.top;
  219. const pixel = context.getImageData(x, y, 1, 1);
  220. const data = pixel.data;
  221. //console.log(pixel, data);
  222. const rgb = 'rgb(' + data[0] + ', ' + data[1] + ', ' + data[2] + ')';
  223. let off = 0 === (data[0] | data[1] | data[2]);
  224. // use 211,211,211 (gray) as a placeholder for 'lights turned off'
  225. destination.style.background = off ?'rgb(211, 211, 211)' :rgb;
  226. destination.textContent = rgb;
  227. // handle optional callback
  228. if(false !== cb) cb(data[0], data[1], data[2]);
  229. return rgb;
  230. }
  231. canvas.addEventListener('mousemove', (event) => pick(event, hovered));
  232. canvas.addEventListener('click', (event) => pick(event, selected));
  233. </script>
  234. ";
  235. // add a callback to the click event to send rgb value to esp8266
  236. echo "
  237. <script>
  238. function sg(x) { return Math.round((x*x)/255); }
  239. function callback(red, green, blue) {
  240. console.log('logged', red, green, blue);
  241. post(
  242. /* url */ '',
  243. /* key */ 'color',
  244. sg(red).toString(16).padStart(2,'0') +
  245. sg(green).toString(16).padStart(2,'0') +
  246. sg(blue).toString(16).padStart(2,'0')
  247. ).then((data) => { console.log(data, data.text()); });
  248. }
  249. canvas.removeEventListener('click', (event) => pick(event, selected));
  250. canvas.addEventListener('click', (event) => pick(event, selected, callback));
  251. </script>
  252. ";
  253. /* make a brightness slider, esp8266 takes 0..15 levels of brightness
  254. * zero is off and should be ignored
  255. *
  256. * currently brightness control is very simple and operates without
  257. * modifying end-user color choices
  258. *
  259. * tldr; users have 24bit color selectivity, in hardware we have a
  260. * little over 1.5 bytes per color giving us somewhere near 37bit color
  261. * selectivity. so a user can select 0a3bcc as their color choise and
  262. * we can have thousands of shades of colors that are close to that.
  263. * The power section of the esp8266 is not optimal so the esp8266 will
  264. * limit running the LEDs at maximum current. This limits the color-space
  265. * by about 20%. The way we implement brightness is to cut give the end
  266. * user 24-bits of color selectivity, that fits into 1/20th of the color-space
  267. * then we can multiply those values linearly 1..15 before we hit that 80%
  268. * power limit. because we are amplifying colors evenly the selected output
  269. * color will not change.
  270. */
  271. echo "
  272. <script>
  273. var slider = document.createElement('input');
  274. slider.type = 'range';
  275. slider.min = 1;
  276. slider.max = 15;
  277. slider.value = 7;
  278. slider.step = 1;
  279. document.body.appendChild(slider);
  280. // set the inital value of the slider to whatever the current brightness value
  281. getBrightness(slider);
  282. function slid(event, cb = false) {
  283. if(false !== cb) cb(slider.value);
  284. console.log('changed slider to ' + slider.value);
  285. return 'brightness(' + slider.value + ')';
  286. }
  287. slider.addEventListener('change', (event) => slid(event));
  288. </script>
  289. ";
  290. // add a callback to the off-click event on the slider
  291. echo "
  292. <script>
  293. function slidercb(valu) {
  294. console.log('logged', valu);
  295. post('', 'brightness', parseInt(valu, 10).toString(16).padStart(2,'0')).then((data) => { console.log(data, data.text()); });
  296. }
  297. slider.removeEventListener('change', (event) => slid(event));
  298. slider.addEventListener('change', (event) => slid(event, slidercb));
  299. </script>
  300. ";
  301. echo "</body>";
  302. echo "</html>";