redoid
Version:
A Node.js package that provides an API to control your IKEA Dioder LED light strip on your Raspberry Pi.
587 lines (496 loc) • 12.7 kB
JavaScript
/*!
* redoid
* Copyright (c) 2016 Fränz Friederes <fraenz@frieder.es>
* MIT Licensed
*/
'use strict';
var piblaster = require('pi-blaster.js');
var easingFunctions = require('./easing-functions');
/**
* Redoid prototype
*/
var StaticRedoid = function(options) {
this._init.apply(this, arguments);
};
var Redoid = StaticRedoid.prototype;
/**
* Initialize the dioder.
*
* @param {Array|null} options
* @private
*/
Redoid._init = function(options)
{
options = options || {};
// attributes
this._color = null;
this._queue = [];
this._loopIntervalPointer = null;
this._idleTimeoutPointer = null;
this._isInIdleTransition = false;
// options
var color = this._interpretColor(options.color || '#000000');
this._colorComponentPins = options.colorComponentPins || [4, 17, 18];
this._applyColorCallback = options.applyColorCallback || null;
this._loopInterval = options.loopInterval || 25;
this._defaultEasing = options.defaultEasing || 'easeInOutQuad';
this._idleCallback = options.idleCallback || null;
this._idleTimeout = options.idleTimeout || 0;
this._idleColor = this._interpretColor(options.idleColor) || null;
this._idleColorTransitionDuration = options.idleColorTransitionDuration || 4000;
this._loopTransition = options.loopTransition || false;
// apply initial color
this._applyColor(color);
};
/**
* Fires at each tick of the transition loop.
*
* @private
*/
Redoid._loop = function()
{
var time = this._loopInterval;
// fill up elapsed time of entries in queue until we reach the current one
var entry = null;
while (entry === null && this._queue.length > 0)
{
// get current queue entry
var entry = this._queue[0];
// add elapsed time
entry.elapsedTime += time;
// calculate time overflow
time = Math.max(entry.elapsedTime - entry.duration, 0);
// check if this entry has been completed
if (entry.elapsedTime >= entry.duration)
{
// remove entry from queue
this._queue.splice(0, 1);
// call the callback if available
if (entry.callback !== null) {
entry.callback(this);
}
if (this._loopTransition && !this._isInIdleTransition)
{
// reset elapsed time and requeue this entry
entry.elapsedTime = 0;
entry.from = this.getLastQueuedColor();
this._queue.push(entry);
}
if (this._queue.length === 0)
{
// apply the to color of the last entry in queue
this._applyColor(entry.to);
}
// this transition step is completed
entry = null;
}
}
if (entry === null)
{
// reached end of queue
// clear loop interval
clearInterval(this._loopIntervalPointer);
this._loopIntervalPointer = null;
// transition completed
if (this._idleTimeout > 0)
{
this._idleTimeoutPointer = setTimeout(
this._onIdle.bind(this), this._idleTimeout);
}
else
{
this._onIdle();
}
return;
}
// loop tick fires inside transition step
// calculate intermediate color
var intermediateColor = [0, 0, 0];
// ease time
var percent = (entry.elapsedTime / entry.duration);
var easedTime = this._easeTime(percent, entry.easing);
// go through components
for (var i = 0; i < 3; i ++)
{
// calculate intermediate component value
intermediateColor[i] = parseInt(
(1 - easedTime) * entry.from[i]
+ easedTime * entry.to[i]
);
}
// apply color
this._applyColor(intermediateColor);
};
/**
* Ease time using a named or directly provided function.
*
* @param {int} time
* @param {String|Function} easing
* @return {int}
* @private
*/
Redoid._easeTime = function(time, easing)
{
if (typeof easing === 'function')
{
// use provieded easing function
return easing(time);
}
if (
typeof easing === 'string' &&
easingFunctions[easing] !== undefined
) {
// use known easing function
return (easingFunctions[easing])(time);
}
// use default easing
return easingFunctions['easeInOutQuad'];
};
/**
* Apply color.
*
* @param {Array} color
* @return {Redoid} for chaining
* @private
*/
Redoid._applyColor = function(color)
{
if (this._applyColorCallback === null)
{
// apply each changed color component
for (var i = 0; i < 3; i ++) {
if (this._color === null || color[i] != this._color[i]) {
piblaster.setPwm(this._colorComponentPins[i], color[i] / 255.0);
}
}
}
else
{
// apply color by calling the user defined callback
this._applyColorCallback(color, this._color);
}
// track current color
this._color = color;
return this;
};
/**
* Queue transition.
*
* @param {Array|String} color
* @param {int} duration
* @param {String|Function} easing
* @param {Function|null} callback
* @return {Redoid} for chaining
* @private
*/
Redoid._queueTransition = function(color, duration, easing, callback)
{
// stop idle transition before queuing next transition
if (this._isInIdleTransition) {
this._isInIdleTransition = false;
this.stop();
}
// queue
this._queue.push({
from: this.getLastQueuedColor(),
to: this._interpretColor(color),
elapsedTime: 0,
duration: duration,
easing: easing,
callback: callback || null
});
// clear idle timeout
clearTimeout(this._idleTimeoutPointer);
// start transition loop if not already running
if (this._loopIntervalPointer === null)
{
this._loopIntervalPointer =
setInterval(this._loop.bind(this), this._loopInterval);
}
return this;
};
/**
* Returns `true` when currently inside transition.
*
* @return {Boolean}
* @public
*/
Redoid.isTransitioning = function()
{
return (this._loopIntervalPointer !== null && !this._isInIdleTransition);
};
/**
* Event triggered when redoid is idle.
*
* @private
*/
Redoid._onIdle = function()
{
// call idle callback if available
if (this._idleCallback !== null) {
this._idleCallback(this);
}
// idle callback could have changed idle state
if (!this.isTransitioning() && this._idleColor !== null)
{
// check if the current color is different from the idle color
if (!this.isColorEqual(this._color, this._idleColor))
{
// transition to idle color
this.transition(this._idleColor, this._idleColorTransitionDuration);
// this transition gets cancelled when anything else gets queued
this._isInIdleTransition = true;
return;
}
}
};
/**
* Interpret color.
*
* @param {Array|String} color
* @return {Array}|null
* @private
*/
Redoid._interpretColor = function(color)
{
if (typeof color === 'string')
{
return this._hexValueToColor(color);
}
if (
Object.prototype.toString.call(color) === '[object Array]'
&& color.length == 3
) {
return [
parseInt(color[0]),
parseInt(color[1]),
parseInt(color[2])
];
}
return null;
};
/**
* Convert hex value to color.
*
* @param {String} hexValue
* @return {Array}
* @private
*/
Redoid._hexValueToColor = function(hexValue)
{
// expand shorthand (#03F) to full form (#0033FF)
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hexValue = hexValue.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
// retrieve components
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexValue);
return (result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
] : null);
};
/**
* Convert color to hex value.
*
* @param {Array} color
* @return {String}
* @private
*/
Redoid._colorToHexValue = function(color)
{
var hexValue = '#';
for (var i = 0; i < 3; i ++) {
var componentHexValue = color[i].toString(16);
hexValue += (componentHexValue.length === 1 ? '0' : '') + componentHexValue;
}
return hexValue;
};
/**
* Check if color is valid.
*
* @param {Array|String} color
* @return {Boolean}
* @public
*/
Redoid.isColorValid = function(color)
{
return (this._interpretColor(color) !== null);
};
/**
* Check if colors are equal.
*
* @param {Array|String} a
* @param {Array|String} b
* @return {Boolean}
* @public
*/
Redoid.isColorEqual = function(a, b)
{
// interpret colors
var a = this._interpretColor(a);
var b = this._interpretColor(b);
// compare color components
return (
a[0] == b[0] &&
a[1] == b[1] &&
a[2] == b[2]
);
};
/**
* Return the current color. (e.g. `[255, 0, 0]`)
*
* @return {Array}
* @public
*/
Redoid.getColor = function()
{
return this._color;
};
/**
* Return hex value of current color. (e.g. `#ff0000`)
*
* @return {String}
* @public
*/
Redoid.getColorHexValue = function()
{
return this._colorToHexValue(this._color);
};
/**
* Return the last queued color. If `loopTransition` is set to `true`,
* this value changes during transition.
*
* @return {Array}
* @public
*/
Redoid.getLastQueuedColor = function()
{
if (this._queue.length > 0) {
return this._queue[this._queue.length - 1].to;
}
return this._color;
};
/**
* Return hex value of last queued color.
*
* @return {String}
* @public
*/
Redoid.getLastQueuedColorHexValue = function()
{
return this._colorToHexValue(this.getLastQueuedColor());
};
/**
* Queue transition from the last queued color (obtained
* by `getLastQueuedColor`) to the color provided.
*
* @param {Array|String} color
* @param {int|null} duration
* @param {String|Function|null} easing
* @return {Redoid} for chaining
* @public
*/
Redoid.transition = function(color, duration, easing)
{
return this._queueTransition(
color,
duration || 1000,
easing || this._defaultEasing
);
};
/**
* Queue color change to color provided.
*
* @param {Array|String} color
* @return {Redoid} for chaining
* @public
*/
Redoid.change = function(color)
{
return this._queueTransition(color, 0, 'linear');
};
/**
* Queue turn off.
*
* @param {Int|null} duration
* @return {Redoid} for chaining
* @public
*/
Redoid.turnOff = function(duration)
{
return this._queueTransition('#000000', duration || 0, 'linear');
};
/**
* Delay next queue entry by given duration.
*
* @param {int} delay
* @return {Redoid} for chaining
* @public
*/
Redoid.delay = function(delay)
{
// animate to same color to create delay between transition steps
return this._queueTransition(this.getLastQueuedColor(), delay, 'linear');
};
/**
* Trigger callback when transition reaches this transition step.
*
* @param {Function|null} callback
* @return {Redoid} for chaining
* @public
*/
Redoid.trigger = function(callback)
{
return this._queueTransition(this.getLastQueuedColor(), 0, 'linear', callback);
};
/**
* Interrupt the current transition and clear the queue.
* When firing this method, no callbacks set by `trigger` get called.
*
* @return {Redoid} for chaining
* @public
*/
Redoid.stop = function()
{
// clear queued entries if there are any
if (this._queue.length > 0) {
this._queue = [];
}
return this;
};
/**
* If set to `true` completed transition steps will be added
* to the end of the queue resulting in a never-ending transition loop.
*
* @param {Boolean} loopTransition
* @return {Redoid} for chaining
* @public
*/
Redoid.setLoopTransition = function(loopTransition)
{
this._loopTransition = loopTransition;
return this;
};
/**
* Idle color to transition to when the idle event is triggered.
* This feature is disabled if set to `null`.
*
* @param {Array|String|null} idleColor
* @return {Redoid} for chaining
* @public
*/
Redoid.setIdleColor = function(idleColor)
{
this._idleColor = (this._idleColor !== null ?
this._interpretColor(idleColor) : null);
return this;
};
/**
* Populate module exports
*/
module.exports = function(options) {
return new StaticRedoid(options);
};
module.exports.easingFunctions = easingFunctions;