UNPKG

13.4 kBJavaScriptView Raw
1var Board = require("../board");
2var Animation = require("../animation");
3var Expander = require("../expander");
4var Fn = require("../fn");
5var converter = require("color-convert");
6
7var priv = new Map();
8
9var Controllers = {
10 DEFAULT: {
11 initialize: {
12 value: function(opts) {
13 RGB.colors.forEach(function(color, index) {
14 var pin = opts.pins[index];
15
16 if (opts.debug && !this.board.pins.isPwm(pin)) {
17 Board.Pins.Error({
18 pin: pin,
19 type: "PWM",
20 via: "Led.RGB"
21 });
22 }
23
24 this.io.pinMode(pin, this.io.MODES.PWM);
25 this.pins[index] = pin;
26 }, this);
27 }
28 },
29 write: {
30 writable: true,
31 value: function(colors) {
32 var state = priv.get(this);
33
34 RGB.colors.forEach(function(color, index) {
35 var pin = this.pins[index];
36 var value = colors[color];
37
38 if (state.isAnode) {
39 value = 255 - Board.constrain(value, 0, 255);
40 }
41 value = Fn.map(value, 0, 255, 0, this.board.RESOLUTION.PWM);
42
43 this.io.analogWrite(pin, value);
44 }, this);
45 }
46 }
47 },
48 PCA9685: {
49 initialize: {
50 value: function(opts) {
51
52 var state = priv.get(this);
53
54 this.address = opts.address || 0x40;
55 this.pwmRange = opts.pwmRange || [0, 4095];
56 this.frequency = opts.frequency || 200;
57
58 state.expander = Expander.get({
59 address: this.address,
60 controller: this.controller,
61 bus: this.bus,
62 pwmRange: this.pwmRange,
63 frequency: this.frequency,
64 });
65
66 RGB.colors.forEach(function(color, index) {
67 this.pins[index] = state.expander.normalize(opts.pins[index]);
68 state.expander.analogWrite(this.pins[index], 0);
69 }, this);
70 }
71 },
72 write: {
73 writable: true,
74 value: function(colors) {
75 var state = priv.get(this);
76
77 RGB.colors.forEach(function(color, index) {
78 var pin = this.pins[index];
79 var value = colors[color];
80
81 if (state.isAnode) {
82 value = 255 - Board.constrain(value, 0, 255);
83 }
84
85 state.expander.analogWrite(pin, value);
86
87 }, this);
88 }
89 }
90 },
91 BLINKM: {
92 REGISTER: {
93 value: {
94 GO_TO_RGB_COLOR_NOW: 0x6e,
95 STOP_SCRIPT: 0x6f
96 }
97 },
98 initialize: {
99 value: function(opts) {
100 this.address = opts.address || 0x09;
101
102 // Ensure that this is passed on to i2cConfig
103 opts.address = this.address;
104
105 /* istanbul ignore else */
106 if (!this.board.Drivers[this.address]) {
107 this.io.i2cConfig(opts);
108 this.board.Drivers[this.address] = {
109 initialized: false
110 };
111
112 // Stop the current script
113 this.io.i2cWrite(this.address, [this.REGISTER.STOP_SCRIPT]);
114
115 this.board.Drivers[this.address].initialized = true;
116 }
117 }
118 },
119 write: {
120 writable: true,
121 value: function(colors) {
122 this.io.i2cWrite(this.address, [this.REGISTER.GO_TO_RGB_COLOR_NOW, colors.red, colors.green, colors.blue]);
123 }
124 }
125 }
126};
127
128Controllers.ESPLORA = {
129 initialize: {
130 value: function(opts) {
131 opts.pins = [5, 10, 9];
132 this.pins = [];
133 Controllers.DEFAULT.initialize.value.call(this, opts);
134 }
135 },
136 write: Controllers.DEFAULT.write
137};
138
139/**
140 * RGB
141 * @constructor
142 *
143 * @param {Object} opts [description]
144 * @alias Led.RGB
145 */
146function RGB(opts) {
147 if (!(this instanceof RGB)) {
148 return new RGB(opts);
149 }
150
151 var controller = null;
152
153 if (Array.isArray(opts)) {
154 // RGB([Byte, Byte, Byte]) shorthand
155 // Convert to opts.pins array definition
156 opts = {
157 pins: opts
158 };
159 // If opts.pins is an object, convert to array
160 } else if (typeof opts.pins === "object" && !Array.isArray(opts.pins)) {
161 opts.pins = [opts.pins.red, opts.pins.green, opts.pins.blue];
162 }
163
164 Board.Component.call(
165 this, opts = Board.Options(opts)
166 );
167
168 if (opts.controller && typeof opts.controller === "string") {
169 controller = Controllers[opts.controller.toUpperCase()];
170 } else {
171 controller = opts.controller;
172 }
173
174 if (controller == null) {
175 controller = Controllers.DEFAULT;
176 }
177
178
179 // The default color is #ffffff, but the light will be off
180 var state = {
181 red: 255,
182 green: 255,
183 blue: 255,
184 intensity: 100,
185 isAnode: opts.isAnode || false,
186 interval: null
187 };
188
189 // red, green, and blue store the raw color set via .color()
190 // values takes state into account, such as on/off and intensity
191 state.values = {
192 red: state.red,
193 green: state.green,
194 blue: state.blue
195 };
196
197 priv.set(this, state);
198
199 Board.Controller.call(this, controller, opts);
200
201 Object.defineProperties(this, {
202 isOn: {
203 get: function() {
204 return RGB.colors.some(function(color) {
205 return state[color] > 0;
206 });
207 }
208 },
209 isRunning: {
210 get: function() {
211 return !!state.interval;
212 }
213 },
214 isAnode: {
215 get: function() {
216 return state.isAnode;
217 }
218 },
219 values: {
220 get: function() {
221 return Object.assign({}, state.values);
222 }
223 },
224 update: {
225 value: function(colors) {
226 var state = priv.get(this);
227
228 colors = colors || this.color();
229
230 state.values = RGB.ToScaledRGB(state.intensity, colors);
231
232 this.write(state.values);
233
234 Object.assign(state, colors);
235 }
236 }
237 });
238
239 this.initialize(opts);
240 this.off();
241}
242
243RGB.colors = ["red", "green", "blue"];
244
245
246RGB.ToScaledRGB = function(intensity, colors) {
247 var scale = intensity / 100;
248
249 return RGB.colors.reduce(function(current, color) {
250 return (current[color] = Math.round(colors[color] * scale), current);
251 }, {});
252};
253
254RGB.ToRGB = function(red, green, blue) {
255 var update = {};
256 var flags = 0;
257 var input;
258
259 if (typeof red !== "undefined") {
260 // 0b100
261 flags |= 1 << 2;
262 }
263
264 if (typeof green !== "undefined") {
265 // 0b010
266 flags |= 1 << 1;
267 }
268
269 if (typeof blue !== "undefined") {
270 // 0b001
271 flags |= 1 << 0;
272 }
273
274 if ((flags | 0x04) === 0x04) {
275 input = red;
276
277 if (input == null) {
278 throw new Error("Invalid color (" + input + ")");
279 }
280
281 /* istanbul ignore else */
282 if (Array.isArray(input)) {
283 // color([Byte, Byte, Byte])
284 update = {
285 red: input[0],
286 green: input[1],
287 blue: input[2]
288 };
289 } else if (typeof input === "object") {
290 // color({
291 // red: Byte,
292 // green: Byte,
293 // blue: Byte
294 // });
295 update = {
296 red: input.red,
297 green: input.green,
298 blue: input.blue
299 };
300 } else if (typeof input === "string") {
301
302 // color("#ffffff") or color("ffffff")
303 if (/^#?[0-9A-Fa-f]{6}$/.test(input)) {
304
305 // remove the leading # if there is one
306 if (input.length === 7 && input[0] === "#") {
307 input = input.slice(1);
308 }
309
310 update = {
311 red: parseInt(input.slice(0, 2), 16),
312 green: parseInt(input.slice(2, 4), 16),
313 blue: parseInt(input.slice(4, 6), 16)
314 };
315 } else {
316 // color("rgba(r, g, b, a)") or color("rgb(r, g, b)")
317 // color("rgba(r g b a)") or color("rgb(r g b)")
318 if (/^rgb/.test(input)) {
319 var args = input.match(/^rgba?\(([^)]+)\)$/)[1].split(/[\s,]+/);
320
321 // If the values were %...
322 if (isPercentString(args[0])) {
323 args.forEach(function(value, index) {
324 // Only convert the first 3 values
325 if (index <= 2) {
326 args[index] = Math.round((parseInt(value, 10) / 100) * 255);
327 }
328 });
329 }
330
331 update = {
332 red: parseInt(args[0], 10),
333 green: parseInt(args[1], 10),
334 blue: parseInt(args[2], 10)
335 };
336
337 // If rgba(...)
338 if (args.length > 3) {
339 if (isPercentString(args[3])) {
340 args[3] = parseInt(args[3], 10) / 100;
341 }
342 update = RGB.ToScaledRGB(100 * parseFloat(args[3]), update);
343 }
344 } else {
345 // color name
346 return RGB.ToRGB(converter.keyword.rgb(input.toLowerCase()));
347 }
348 }
349 }
350 } else {
351 // color(red, green, blue)
352 update = {
353 red: red,
354 green: green,
355 blue: blue
356 };
357 }
358
359 return update;
360};
361
362function isPercentString(input) {
363 return typeof input === "string" && input.endsWith("%");
364}
365/**
366 * color
367 *
368 * @param {String} color Hexadecimal color string or CSS color name
369 * @param {Array} color Array of color values
370 * @param {Object} color object {red, green, blue}
371 *
372 * @return {RGB}
373 */
374RGB.prototype.color = function(red, green, blue) {
375 var state = priv.get(this);
376 var colors;
377
378 if (arguments.length === 0) {
379 // Return a copy of the state values,
380 // not a reference to the state object itself.
381 colors = this.isOn ? state : state.prev;
382 return RGB.colors.reduce(function(current, color) {
383 return (current[color] = Math.round(colors[color]), current);
384 }, {});
385 }
386
387 var update = RGB.ToRGB(red, green, blue);
388
389 // Validate all color values before writing any values
390 RGB.colors.forEach(function(color) {
391 var value = update[color];
392
393 if (value == null) {
394 throw new Error("Led.RGB.color: invalid color ([" + [update.red, update.green, update.blue].join(",") + "])");
395 }
396
397 value = Fn.constrain(value, 0, 255);
398 update[color] = value;
399 }, this);
400
401 this.update(update);
402
403 return this;
404};
405
406RGB.prototype.on = function() {
407 var state = priv.get(this);
408 var colors;
409
410 // If it's not already on, we set them to the previous color
411 if (!this.isOn) {
412 /* istanbul ignore next */
413 colors = state.prev || {
414 red: 255,
415 green: 255,
416 blue: 255
417 };
418
419 state.prev = null;
420
421 this.update(colors);
422 }
423
424 return this;
425};
426
427RGB.prototype.off = function() {
428 var state = priv.get(this);
429
430 // If it's already off, do nothing so the pervious state stays intact
431 /* istanbul ignore else */
432 if (this.isOn) {
433 state.prev = RGB.colors.reduce(function(current, color) {
434 return (current[color] = state[color], current);
435 }.bind(this), {});
436
437 this.update({
438 red: 0,
439 green: 0,
440 blue: 0
441 });
442 }
443
444 return this;
445};
446
447/**
448 * blink
449 * @param {Number} duration Time in ms on, time in ms off
450 * @return {RGB}
451 */
452RGB.prototype.blink = function(duration, callback) {
453 var state = priv.get(this);
454
455 // Avoid traffic jams
456 this.stop();
457
458 if (typeof duration === "function") {
459 callback = duration;
460 duration = null;
461 }
462
463 state.interval = setInterval(function() {
464 this.toggle();
465 if (typeof callback === "function") {
466 callback();
467 }
468 }.bind(this), duration || 100);
469
470 return this;
471};
472
473RGB.prototype.strobe = RGB.prototype.blink;
474
475RGB.prototype.toggle = function() {
476 return this[this.isOn ? "off" : "on"]();
477};
478
479RGB.prototype.stop = function() {
480 var state = priv.get(this);
481
482 if (state.interval) {
483 clearInterval(state.interval);
484 }
485
486 /* istanbul ignore if */
487 if (state.animation) {
488 state.animation.stop();
489 }
490
491 state.interval = null;
492
493 return this;
494};
495
496RGB.prototype.intensity = function(intensity) {
497 var state = priv.get(this);
498
499 if (arguments.length === 0) {
500 return state.intensity;
501 }
502
503 state.intensity = Fn.constrain(intensity, 0, 100);
504
505 this.update();
506
507 return this;
508};
509
510/**
511 * Animation.normalize
512 *
513 * @param [number || object] keyFrames An array of step values or a keyFrame objects
514 */
515
516RGB.prototype[Animation.normalize] = function(keyFrames) {
517 var state = priv.get(this);
518
519 // If user passes null as the first element in keyFrames use current value
520 if (keyFrames[0] === null) {
521 keyFrames[0] = state.values;
522 }
523
524 return keyFrames.reduce(function(accum, frame) {
525 var normalized = {};
526 var value = frame;
527 var color = null;
528 var intensity = state.intensity;
529
530 if (frame !== null) {
531 // Frames that are just numbers are not allowed
532 // because it is ambiguous.
533 if (typeof value === "number") {
534 throw new Error("RGB LEDs expect a complete keyFrame object or hexadecimal string value");
535 }
536
537 if (typeof value === "string") {
538 color = value;
539 }
540
541 if (Array.isArray(value)) {
542 color = value;
543 } else {
544 if (typeof value === "object") {
545 if (typeof value.color !== "undefined") {
546 color = value.color;
547 } else {
548 color = value;
549 }
550 }
551 }
552
553 if (typeof frame.intensity === "number") {
554 intensity = frame.intensity;
555 delete frame.intensity;
556 }
557
558 normalized.easing = frame.easing || "linear";
559 normalized.value = RGB.ToScaledRGB(intensity, RGB.ToRGB(color));
560 } else {
561 normalized = frame;
562 }
563
564 accum.push(normalized);
565
566 return accum;
567 }, []);
568};
569
570/**
571 * Animation.render
572 *
573 * @color [object] color object
574 */
575
576RGB.prototype[Animation.render] = function(frames) {
577 return this.color(frames[0]);
578};
579
580/**
581 * For multi-property animation, must define
582 * the keys to use for tween calculation.
583 */
584RGB.prototype[Animation.keys] = RGB.colors;
585
586/* istanbul ignore else */
587if (!!process.env.IS_TEST_MODE) {
588 RGB.Controllers = Controllers;
589 RGB.purge = function() {
590 priv.clear();
591 };
592}
593
594module.exports = RGB;