├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE.txt
├── README.md
├── es5.js
├── index.js
├── lib
├── CssCueLoader.js
└── DeviceRegistry.js
├── package.json
├── test
├── .eslintrc
├── CssCueLoaderTest.js
└── DeviceRegistryTest.js
└── wallaby.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 |
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "es6": true,
8 | "mocha": true
9 | },
10 |
11 | "plugins": [
12 | ],
13 |
14 | "globals": {
15 | },
16 |
17 | "ecmaFeatures": {
18 | "modules": true
19 | },
20 |
21 | "rules": {
22 | "comma-dangle": 2,
23 | "no-cond-assign": [2, "except-parens"],
24 | "no-console": 1,
25 | "no-constant-condition": 2,
26 | "no-control-regex": 2,
27 | "no-debugger": 1,
28 | "no-dupe-keys": 2,
29 | "no-empty": 2,
30 | "no-empty-character-class": 2,
31 | "no-ex-assign": 2,
32 | "no-extra-boolean-cast": 2,
33 | "no-extra-parens": 1,
34 | "no-extra-semi": 2,
35 | "no-func-assign": 2,
36 | "no-inner-declarations": 2,
37 | "no-invalid-regexp": 2,
38 | "no-irregular-whitespace": 2,
39 | "no-obj-calls": 2,
40 | "no-regex-spaces": 2,
41 | "quote-props": 2,
42 | "no-sparse-arrays": 2,
43 | "no-unreachable": 2,
44 | "use-isnan": 2,
45 | "require-jsdoc": 2,
46 | "valid-jsdoc": [2, {
47 | "prefer": { "return": "returns" },
48 | "requireReturn": false,
49 | "requireParamDescription": false,
50 | "requireReturnDescription": false
51 | }],
52 | "valid-typeof": 2,
53 |
54 | "block-scoped-var": 2,
55 | "complexity": [1, 7],
56 | "consistent-return": 2,
57 | "curly": [2, "all"],
58 | "default-case": 2,
59 | "dot-notation": [2, {"allowKeywords": true}],
60 | "eqeqeq": 2,
61 | "guard-for-in": 1,
62 | "no-alert": 1,
63 | "no-caller": 2,
64 | "no-div-regex": 1,
65 | "no-else-return": 2,
66 | "no-empty-label": 2,
67 | "no-eq-null": 2,
68 | "no-eval": 2,
69 | "no-extend-native": 2,
70 | "no-extra-bind": 2,
71 | "no-fallthrough": 2,
72 | "no-floating-decimal": 2,
73 | "no-implied-eval": 2,
74 | "no-iterator": 2,
75 | "no-labels": 2,
76 | "no-lone-blocks": 2,
77 | "no-loop-func": 2,
78 | "no-multi-spaces": 2,
79 | "no-multi-str": 2,
80 | "no-native-reassign": 2,
81 | "no-new": 2,
82 | "no-new-func": 2,
83 | "no-new-wrappers": 2,
84 | "no-octal": 2,
85 | "no-octal-escape": 2,
86 | "no-process-env": 1,
87 | "no-proto": 2,
88 | "no-redeclare": 2,
89 | "no-return-assign": 2,
90 | "no-script-url": 2,
91 | "no-self-compare": 2,
92 | "no-sequences": 2,
93 | "no-unused-expressions": 2,
94 | "no-void": 0,
95 | "no-warning-comments": 1,
96 | "no-with": 2,
97 | "radix": 2,
98 | "vars-on-top": 0,
99 | "wrap-iife": [2, "inside"],
100 | "yoda": [2, "never"],
101 |
102 | "strict": [0, "never"],
103 |
104 | "no-catch-shadow": 2,
105 | "no-delete-var": 2,
106 | "no-label-var": 2,
107 | "no-shadow": 2,
108 | "no-shadow-restricted-names": 2,
109 | "no-undef": 2,
110 | "no-undef-init": 2,
111 | "no-undefined": 2,
112 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
113 | "no-use-before-define": [2, "nofunc"],
114 |
115 | "handle-callback-err": 1,
116 | "no-mixed-requires": 0,
117 | "no-new-require": 2,
118 | "no-path-concat": 2,
119 | "no-process-exit": 1,
120 | "no-restricted-modules": 0,
121 | "no-sync": 0,
122 |
123 | "indent": [2, 2],
124 | "brace-style": [2, "1tbs", { "allowSingleLine": false }],
125 | "camelcase": 2,
126 | "comma-spacing": [2, {"before": false, "after": true}],
127 | "comma-style": [2, "last"],
128 | "consistent-this": [2, "none"],
129 | "eol-last": 1,
130 | "func-names": 0,
131 | "func-style": 0,
132 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
133 | "max-nested-callbacks": [2, 6],
134 | "new-cap": 2,
135 | "new-parens": 2,
136 | "no-array-constructor": 2,
137 | "no-inline-comments": 0,
138 | "no-lonely-if": 2,
139 | "no-mixed-spaces-and-tabs": 2,
140 | "no-multiple-empty-lines": [1, {"max": 2}],
141 | "no-nested-ternary": 2,
142 | "no-new-object": 2,
143 | "semi-spacing": 2,
144 | "no-spaced-func": 2,
145 | "no-ternary": 0,
146 | "no-trailing-spaces": 2,
147 | "no-underscore-dangle": 0,
148 | "no-extra-parens": 2,
149 | "one-var": 0,
150 | "operator-assignment": 0,
151 | "padded-blocks": [1, "never"],
152 | "quote-props": [2, "as-needed"],
153 | "quotes": [2, "single", "avoid-escape"],
154 | "semi": [2, "always"],
155 | "sort-vars": 0,
156 | "space-before-function-paren": [2, "never"],
157 | "space-after-keywords": [2, "always"],
158 | "space-before-blocks": [2, "always"],
159 | "object-curly-spacing": [1, "never"],
160 | "array-bracket-spacing": [1, "never"],
161 | "computed-property-spacing": [1, "never"],
162 | "space-in-parens": [2, "never"],
163 | "space-infix-ops": 2,
164 | "space-return-throw-case": 2,
165 | "space-unary-ops": [2, { "words": true, "nonwords": false }],
166 | "wrap-regex": 0,
167 |
168 | "no-var": 0,
169 | "generator-star-spacing": 2,
170 |
171 | "max-depth": [1, 3],
172 | "max-len": [2, 80, 2],
173 | "max-params": [2, 4],
174 | "max-statements": [1, 15],
175 | "no-bitwise": 2,
176 | "no-plusplus": 0
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /lib-es5
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Martin Schuhfuss
2 |
3 | This program is free software: you can redistribute it and/or modify
4 | it under the terms of the GNU General Public License as published by
5 | the Free Software Foundation, either version 3 of the License, or
6 | (at your option) any later version.
7 |
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 |
13 | You should have received a copy of the GNU General Public License
14 | along with this program. If not, see .
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fivetwelve-css – control lighting using CSS
2 |
3 | This is an initial draft-implementation of a possibility to use CSS to control ambient lighting and professional light-equipment.
4 |
5 | It is written to work with the [fivetwelve dmx-control library](https://github.com/beyondscreen/fivetwelve).
6 |
7 |
8 | ## Usage
9 |
10 | Details about how to write css-code for this can be found below. More info about how devices are configured can be found [here](https://github.com/beyondscreen/fivetwelve). From the JS side the usage looks like this:
11 |
12 | ```javascript
13 | import {DeviceRegistry, CssCueLoader} from 'fivetwelve-css';
14 |
15 | const registry = new DeviceRegistry();
16 | // configure devices and add them to the registry
17 |
18 | const loader = new CssCueLoader(registry, cssText);
19 |
20 | // load the `.intro`-scene
21 | loader.setCue('.intro');
22 |
23 | // after two seconds switch to scene `.a-magician-appears`
24 | setTimeout(() => loader.setCue('.a-magician-appears'), 2000);
25 | ```
26 |
27 | So this is basically all functionality you need to build a simple lighting-control system for stage-lighting. Replace the timeout in the example with midi-events from a controller or a web-interface and you have your light-control software.
28 |
29 |
30 | ## Concept
31 |
32 | ### Background: DMX512
33 |
34 | Stage-lighting is controlled using a robust and simple protocol named DMX512 which simply sends the very same 512 bytes of data (512 channels w/ one byte each) over and over again. Every device uses a fixed address within this array and usually interprets a single channel or a fixed number of channels starting at that address. Address collisions are prevented when assigning addresses to the devices.
35 | Every channel-value controls a feature of the device. These features could be things like the brightness, movement, a color-mixing unit, beam-shaping or any number of features that devices offer. What and how the single features work in terms of DMX-values is totally device-specific and is described in the device manual.
36 |
37 |
38 | ### Devices, DeviceGroups and DeviceRegistry
39 |
40 | This solution maintains a queryable database for the light-fixtures called DeviceRegistry. This registry resembles the DOM when thinking about how light fixtures are selected (in fact, an upcoming version will likely use an actual dom-structure as central device-database, mostly replacing the registry).
41 |
42 | Devices are added to the registry by providing the Device object along with an id and a list of classes. Queries are formed as CSS-selectors and the Registry returns a Device or DeviceGroup Object that is used to write properties to. Which of them doesn't matter because DeviceGroups use the exact same interface as the contained Devices, so properties written to a group are also written to every device in that group.
43 |
44 | ```javascript
45 | const registry = new DeviceRegistry();
46 |
47 | for(let {device, id, classlist} of deviceDefinitions) {
48 | registry.add(device, id, classList);
49 | }
50 |
51 | let groupOrDevice = registry.select('.spotlight.left');
52 |
53 | // set to full brightness
54 | groupOrDevice.brightness = 1;
55 | ```
56 |
57 | So groups of devices are defined by adding the same classname to it. Classnames will usually establish a device-grouping by position (front, floor, backline), device-type (movinghead, fog, dimmer, motors), role and other properties.
58 |
59 |
60 | ### Property-Values: Defaults and Inheritance
61 |
62 | For the first CSS-example we assume just a single property `brightness` that is shared by all devices (like for instance in a conventional theater-setup with only dimmable light-fixtures). We can immediately see the first great thing about using CSS to do this: There already are well-defined rules for how inheritance and overriding of property-values works, so if you know css you will probably know what is happening here without needing further documentation:
63 |
64 | ```css
65 | /* default for all devices */
66 | * { brightness: 0; }
67 |
68 | /* bring the stage into a dim light */
69 | .stage-base-lighting { brightness: .2; }
70 |
71 | /* the stage-center should be in full light */
72 | .stage-base-lighting.stage-center { brightness: .5; }
73 |
74 | /* additional light from the left front */
75 | .front-spots.left { brightness: 1; }
76 | ```
77 |
78 |
79 | ### Defining multiple Scenes
80 |
81 | The previous example only describes a single static light setting. In order to describe multiple settings for a lightshow or to configure different light-presets for your home, we can simply add another level of selectors to the structure.
82 |
83 | ```css
84 | /* base setting for all scenes */
85 | .stage-base-lighting { brightness: .1; }
86 |
87 | .a-magician-appears .stage-base-lighting.stage-center { brightness: .5; }
88 | .a-magician-appears .front-spots.stage-left { brightness: 1; }
89 | ```
90 |
91 | This new level of selectors can now be used to address any number of light settings. Think of this as styling a DOM-Structure like this:
92 |
93 | ```xml
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | ```
103 |
104 | So the outer level can be used to select the light-preset that should be in effect while the inner-level is used to select the light-fixtures being styled.
105 |
106 |
107 | ### Combining cues/presets to compose scenes
108 |
109 | With that in mind, it is also possible (and also mostly for free just by using the power of CSS) to define light-settings in partials, and using them by applying multiple classes at once on the outer level. This is especially useful when thinking of moving-head spotlights or other more complex light-setups. One could for instance define presets for colors, movement-positions, etc. independently and combine them together to create a final light-setting.
110 |
111 |
112 | ## TODO
113 |
114 | As said in the beginning, this is just an early proof-of-concept implementation with a lot of open issues, inconsistencies and missing links. Some of them are described here.
115 |
116 | ### Reused DMX-channels and conflict-resolution
117 |
118 | Devices might use a single channel to control multiple features
119 | that are represented by different CSS-properties. So, for instance,
120 | a lot of devices use a single channel for the mechanical shutter
121 | which exposes multiple parameters to the css (shutter, strobe,
122 | pulse). In this case there needs to be a way to define how these
123 | properties will interact with each other and which of the properties
124 | gets to control the final computed dmx channel-value.
125 |
126 | The same goes for something that is often seen with gobos for
127 | moving-heads: Depending on the values of the gobo-selection channel
128 | the gobo-rotation-channel will either behave as fixed rotation for
129 | positioning or rotation speed for continuous rotation. In this case a
130 | setting of the rotation-speed could overwrite a value for the
131 | rotation-position, depending on the operation-mode of the device.
132 |
133 | A possible solution for this could be to use a parameter-model that is a bit closer to the dmx-channel layout, using keywords and combined properties (so for instance `gobo: stars rotating 1.5s` and `gobo: stars indexed 120deg`) that are mapped to the corresponding channel-values (this could be implemented similar to how colors are handled by exposing objects as property-values).
134 |
135 |
136 | ### other TODOs, unsorted
137 |
138 | * Think about how generalized parameters (maybe parameter-groups?)
139 | could be defined such that:
140 | - parameters could have a shorthand-property
141 | - a single parameter-instance can expose multiple css-parameters
142 | (like a gobo-parameter exposing gobo-rotation, gobo-position,
143 | gobo-motive, ..)
144 | - parameters can export some sort of function to be called by the
145 | css during style-calculation (think of properties like transform,
146 | filter, ...)
147 | - conflicts are predictably resolved
148 | - have a look at browser-implementations for how the background-
149 | property is interpreted for inspiration.
150 |
151 | * There should be something like the element-styles in the DOM/CSS,
152 | making it easier to combine programmed animations with more static
153 | CSS couterparts. This could be done by interpreting all settings
154 | from CSS in a different way than settings that were manually
155 | applied.
156 |
157 | * In fivetwelve there should be a way to add a calibration-stage for
158 | property-values so that setting the same value via css will
159 | result in the same behaviour across devices (think color-temperature
160 | differences between devices etc).
161 |
162 | * have a look at how css-implementations of browsers could be
163 | exploited to do computed style calculations for us.
164 |
165 | * Transitions and Animations should be implemented. A first rough
166 | version could simply use an animation-loop and basic keyframe-
167 | animations with linear interpolation. Edge-cases here could be
168 | tricky.
169 |
170 | * Support the composes-property from the CSS-modules syntax proposed
171 | by Glen Maddern et al at to make it easier to compose scenes from
172 | existing more granular presets/cues.
173 |
--------------------------------------------------------------------------------
/es5.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | CssCueLoader: require('./lib-es5/CssCueLoader'),
3 | DeviceRegistry: require('./lib-es5/DeviceRegistry')
4 | };
5 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export {default as CssCueLoader} from './lib/CssCueLoader';
2 | export {default as DeviceRegistry} from './lib/DeviceRegistry';
3 |
--------------------------------------------------------------------------------
/lib/CssCueLoader.js:
--------------------------------------------------------------------------------
1 | import rework from 'rework';
2 |
3 | // this is a 100% hacked implementation, if you're having WTF-moments
4 | //reading this you are probably right :)
5 |
6 | /**
7 | * The cue-loader manages parsing of css-text and applying cues to devices
8 | * stored in the registry.
9 | */
10 | export default class CssCueLoader {
11 | constructor(registry, cssText = '') {
12 | this.registry = registry;
13 | this.currentCue = '';
14 | this.cues = null;
15 |
16 | if (cssText.length > 0) {
17 | this.loadCss(cssText);
18 | }
19 | }
20 |
21 | loadCss(cssText) {
22 | this.cues = this.parseCss(cssText);
23 |
24 | if (this.currentCue !== '') {
25 | this.setCue(this.currentCue);
26 | }
27 | }
28 |
29 | setCue(selector) {
30 | this.currentCue = selector;
31 |
32 | if (!this.cues) {
33 | return;
34 | }
35 |
36 | // this would normally be a sorted list of cues matching the given selector,
37 | // sorted by specifity and source-order.
38 | let activeCues = [];
39 |
40 | // iterating through selectors in ascending specifity-order
41 | let cueSelectors = Object.keys(this.cues);
42 | cueSelectors.sort(CssCueLoader.compareSpecificity);
43 |
44 | cueSelectors.forEach(cueSelector => {
45 | if (!this.selectorMatches(selector, cueSelector)) {
46 | return;
47 | }
48 |
49 | activeCues.push(this.cues[cueSelector]);
50 | });
51 |
52 | // as devices might be members of multiple groups, it is not that easy to
53 | // build up a computed style per device. However, if we just apply all
54 | // active selectors in ascending order of specifity, we will end up with
55 | // all devices having the proper values set, as all of this happens
56 | // synchronously we also don't need to worry about temporarily writing the
57 | // wrong values to the device
58 | activeCues.forEach(cue => {
59 | // specifity is for now just the number of classes in the declaration (we
60 | // don't support anything else than class-selectors). (sorting is stable,
61 | // so source-order is preserved.
62 | cue.sort((a, b) => {
63 | return a.deviceSelector.split('.').length -
64 | b.deviceSelector.split('.').length;
65 | });
66 |
67 | cue.forEach(setting => {
68 | setting.deviceGroup.setParams(setting.declaredParams);
69 | });
70 | });
71 | }
72 |
73 | selectorMatches(selector, cueSelector) {
74 | let selectorClasses = selector.split('.').slice(1),
75 | cueClasses = cueSelector.split('.').slice(1);
76 |
77 | // if all classes of the cue can be found in the selector, this is a match.
78 | let allCueClassesMatched = cueClasses.reduce((res, cls) => {
79 | return res && selectorClasses.indexOf(cls) !== -1;
80 | }, true);
81 |
82 | //console.log('classes', selectorClasses, cueClasses, allCueClassesMatched);
83 | return allCueClassesMatched;
84 | }
85 |
86 | parseCss(cssText) {
87 | let rules = rework(cssText).obj.stylesheet.rules
88 | .filter(rule => rule.type === 'rule');
89 |
90 | return this.parseCues(rules);
91 | }
92 |
93 | parseCues(rules) {
94 | let cues = {};
95 |
96 | rules.forEach(rule => {
97 | rule.selectors.forEach(s => {
98 | // all selectors have one or two levels, if one-level, they simply
99 | // describe defaults for the whole show for a device-selection. Two
100 | // levels always have the scene selector first and device-selector
101 | // second.
102 | let [cueSelector, deviceSelector] = s.split(/\s/);
103 |
104 | if (!deviceSelector) {
105 | deviceSelector = cueSelector;
106 | cueSelector = '*';
107 | }
108 |
109 | if (!cues[cueSelector]) {
110 | cues[cueSelector] = [];
111 | }
112 |
113 | let deviceGroup = this.registry.select(deviceSelector);
114 | let declaredParams = {};
115 | rule.declarations.forEach(({property, value}) => {
116 | let tmp = parseFloat(value.replace(/(-?(\d+)?(?:\.\d+)?).*$/, '$1'));
117 | declaredParams[property] = isNaN(tmp) ? value : tmp;
118 | });
119 |
120 | // skip not matching selectors
121 | if (deviceGroup) {
122 | cues[cueSelector].push({
123 | deviceGroup, deviceSelector, declaredParams
124 | });
125 | }
126 | });
127 | });
128 |
129 | return cues;
130 | }
131 |
132 | /**
133 | * Compare specificity of two selectors. Returns a positive value if is
134 | * selectorA is more specific, a negative value if B is more specific and
135 | * zero if both have the same specificity.
136 | *
137 | * @param {String} selectorA
138 | * @param {String} selectorB
139 | * @returns {number}
140 | */
141 | static compareSpecificity(selectorA, selectorB) {
142 | // dummy-implementation supporting only class-selectors...
143 | return selectorA.split('.').length - selectorB.split('.').length;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/lib/DeviceRegistry.js:
--------------------------------------------------------------------------------
1 | import {DeviceGroup} from 'fivetwelve/es5';
2 |
3 | import 'array.from';
4 |
5 |
6 | /**
7 | * The device-registry is used to allow random access to a bigger collection
8 | * of devices. This borrows the simple idea of ids and classnames from the DOM
9 | * to make devices selectable. Every device added to the registry is associated
10 | * with an id and a list of classnames. Devices or devicegroups are then
11 | * selected using css-selector syntax.
12 | */
13 | export default class DeviceRegistry {
14 | /**
15 | * Creates a DeviceRegistry.
16 | */
17 | constructor() {
18 | /**
19 | * @type {Array.}
20 | */
21 | this.devices = [];
22 | /**
23 | * @type {Object.>} maps device-names to devices
24 | * having that device-name.
25 | */
26 | this.devicesByClass = {};
27 | /**
28 | * @type {Object.} maps device-ids to devices.
29 | */
30 | this.devicesById = {};
31 | }
32 |
33 | /**
34 | * Returns a device-group containing all registered devices.
35 | * @returns {DeviceGroup} that group.
36 | */
37 | getAll() {
38 | return new DeviceGroup(this.devices);
39 | }
40 |
41 | /**
42 | * Selects a list of fixtures.
43 | * @param {String} selector A selector-string. Only class and id-selectors
44 | * are implemented for now.
45 | * @returns {DmxDevice|DeviceGroup} The selected device or device-group.
46 | */
47 | select(selector) {
48 | if (selector === '*') {
49 | return new DeviceGroup(this.devices);
50 | }
51 |
52 | if (!selector.match(/([\.#][\w]+)+/)) {
53 | throw new Error('unsupported selector-type');
54 | }
55 |
56 | if (selector.charAt(0) === '#') {
57 | return this.devicesById[selector.slice(1)];
58 | }
59 |
60 | // selector lookup, only same-element class-selectors here
61 | let classes, devices;
62 |
63 | classes = selector.split('.').slice(1);
64 | devices = intersect(classes.map(cls => this.devicesByClass[cls] || []));
65 |
66 | return new DeviceGroup(devices);
67 | }
68 |
69 | /**
70 | * Adds a device to the registry.
71 | * @param {DmxDevice} device
72 | * @param {String} id
73 | * @param {Array.} classList
74 | */
75 | add(device, id, classList = []) {
76 | this.devices.push(device);
77 | this.devicesById[id] = device;
78 |
79 | classList.forEach(c => {
80 | if (!this.devicesByClass[c]) {
81 | this.devicesByClass[c] = [];
82 | }
83 |
84 | this.devicesByClass[c].push(device);
85 | });
86 | }
87 | }
88 |
89 |
90 | /**
91 | * get an array of all elements found in all of the passed arrays.
92 | * @template T
93 | * @param {Array.>} arrays a list of arrays to intersect.
94 | * @returns {Array.} the elements found in all arrays.
95 | */
96 | function intersect(arrays) {
97 | if (arrays.length < 2) {
98 | return arrays[0] || [];
99 | }
100 |
101 | let res = [];
102 | arrays[0].forEach(el => {
103 | if (arrays.every(arr => arr.indexOf(el) !== -1)) {
104 | res.push(el);
105 | }
106 | });
107 |
108 | return res;
109 | }
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fivetwelve-css",
3 | "version": "1.0.3",
4 | "description": "",
5 | "keywords": [
6 | "dmx",
7 | "dmx512",
8 | "lighting",
9 | "light-control",
10 | "css",
11 | "fivetwelve"
12 | ],
13 | "main": "index.js",
14 | "directories": {
15 | "test": "test"
16 | },
17 | "scripts": {
18 | "test": "mocha --compilers js:babel/register test/{**/,}*Test.js && eslint lib/ test/",
19 | "compile": "babel -d lib-es5 lib",
20 | "prepublish": "npm run test && npm run compile"
21 | },
22 | "author": "Martin Schuhfuss ",
23 | "license": "GPL-3.0",
24 | "homepage": "https://github.com/beyondscreen/fivetwelve-css",
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/beyondscreen/fivetwelve-css"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/beyondscreen/fivetwelve-css/issues"
31 | },
32 | "devDependencies": {
33 | "babel": "^5.8.23",
34 | "eslint": "^1.7.3",
35 | "expect.js": "^0.3.1",
36 | "mocha": "^2.3.3",
37 | "sinon": "^1.17.2"
38 | },
39 | "peerDependencies": {
40 | "fivetwelve": "*"
41 | },
42 | "dependencies": {
43 | "array.from": "^0.2.0",
44 | "intersect": "^1.0.1",
45 | "object-assign": "^4.0.1",
46 | "rework": "^1.0.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-unused-vars": 0,
4 | "brace-style": 0,
5 | "require-jsdoc": 0,
6 | "max-statements": 0
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/CssCueLoaderTest.js:
--------------------------------------------------------------------------------
1 | import sinon from 'sinon';
2 | import expect from 'expect.js';
3 |
4 | import objectAssign from 'object-assign';
5 |
6 | if (!Object.assign) {
7 | Object.assign = objectAssign;
8 | }
9 |
10 | import {DmxDevice, DeviceGroup} from 'fivetwelve/es5';
11 |
12 | import DeviceRegistry from '../lib/DeviceRegistry';
13 | import CssCueLoader from '../lib/CssCueLoader';
14 |
15 | describe('CssCueLoader', () => {
16 | describe('contructor()', () => {
17 | it('initializes', () => {
18 | let registry = sinon.createStubInstance(DeviceRegistry);
19 | registry.select = sinon.stub();
20 |
21 | let loader = new CssCueLoader(registry, '');
22 |
23 | expect(loader).to.be.a(CssCueLoader);
24 | });
25 | });
26 |
27 | describe('loadCss()', () => {
28 | it('loads the given css-code', () => {
29 | let registry = sinon.createStubInstance(DeviceRegistry),
30 | group = sinon.createStubInstance(DeviceGroup);
31 |
32 | group.setParams = sinon.spy();
33 | registry.select = sinon.stub();
34 | registry.select.returns(group);
35 |
36 | let loader = new CssCueLoader(registry);
37 | loader.loadCss('* { pan: 123; }');
38 | loader.setCue('.foo');
39 |
40 | expect(group.setParams.callCount).to.be(1);
41 | expect(group.setParams.firstCall.args[0]).to.eql({pan: 123});
42 | });
43 | });
44 |
45 | describe('runCue()', () => {
46 | it('applies generic global (global *-selector)', () => {
47 | let registry = sinon.createStubInstance(DeviceRegistry);
48 | registry.select = sinon.stub();
49 |
50 | let cssText = `
51 | /* match all devices */
52 | * {
53 | pan: 0; tilt: 0;
54 | color: magenta;
55 | }
56 | `;
57 |
58 | // setup test-subject and spies
59 | let deviceGroup = sinon.createStubInstance(DeviceGroup);
60 | deviceGroup.setParams = sinon.spy();
61 | registry.select.withArgs('*').returns(deviceGroup);
62 |
63 | let loader = new CssCueLoader(registry, cssText);
64 | loader.setCue('.foo');
65 |
66 | expect(registry.select.callCount).to.be(1);
67 | expect(deviceGroup.setParams.callCount).to.be(1);
68 |
69 | expect(deviceGroup.setParams.firstCall.args[0]).to.eql({
70 | pan: 0, tilt: 0, color: 'magenta'
71 | });
72 | });
73 |
74 | function createDeviceStub(id) {
75 | let device = sinon.createStubInstance(DmxDevice);
76 | device.__id = id;
77 | device.setParams = function(params) {
78 | Object.assign(device, params);
79 | };
80 |
81 | return device;
82 | }
83 |
84 | it('applies global values (global specifity and inheritance)', () => {
85 | // note: in order to test the setting of computed values, we need to
86 | // deal with real / not mocked device-groups.
87 |
88 | let cssText = `
89 | * { pan: 0; tilt: 0; dimmer: .2; }
90 | .spot { tilt: 30deg; dimmer: 1; color: magenta; }
91 | .wash { dimmer: .5; color: yellow; }
92 | `;
93 |
94 | let d1, d2, d3;
95 | let registry = new DeviceRegistry();
96 | registry.add(d1 = createDeviceStub('d1'), 'd1', ['spot']);
97 | registry.add(d2 = createDeviceStub('d2'), 'd2', ['wash']);
98 | registry.add(d3 = createDeviceStub('d3'), 'd2', ['other']);
99 |
100 | let loader = new CssCueLoader(registry, cssText);
101 | loader.setCue('.blubb');
102 |
103 | expect(d1.dimmer).to.be(1);
104 | expect(d2.dimmer).to.be(0.5);
105 | expect(d3.dimmer).to.be(0.2);
106 | expect(d2.pan).to.be(0);
107 | expect(d1.tilt).to.be(30);
108 | });
109 |
110 | it('applies cue-styles (inheritance, source-order, cue-selection)', () => {
111 | let cssText = `
112 | * { color: white; }
113 |
114 | .spot { color: magenta; }
115 | .wash { color: yellow; }
116 | .other { color: soilentgreen; }
117 |
118 | .test1 .wash { color: orange; }
119 | .test1 .left { color: mauve; }
120 | .test1 .spot { color: red; }
121 |
122 | .test1.cue1 .left { color: purple; }
123 | .test1.cue1 .spot { color: pink; }
124 |
125 | .test2 .spot { color: green; }
126 | `;
127 |
128 | let d1, d2, d3;
129 | let registry = new DeviceRegistry();
130 | registry.add(d1 = createDeviceStub('d1'), 'd1', ['spot', 'left']);
131 | registry.add(d2 = createDeviceStub('d2'), 'd2', ['wash', 'left']);
132 | registry.add(d3 = createDeviceStub('d3'), 'd2', ['other']);
133 |
134 | let loader = new CssCueLoader(registry, cssText);
135 |
136 | loader.setCue('.test1');
137 | expect(d1.color).to.be('red');
138 | expect(d2.color).to.be('mauve');
139 | expect(d3.color).to.be('soilentgreen');
140 |
141 | loader.setCue('.test1.cue1');
142 | expect(d1.color).to.be('pink');
143 | expect(d2.color).to.be('purple');
144 | });
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/test/DeviceRegistryTest.js:
--------------------------------------------------------------------------------
1 | import sinon from 'sinon';
2 | import expect from 'expect.js';
3 |
4 | import DeviceRegistry from '../lib/DeviceRegistry';
5 | import {DmxDevice, DeviceGroup} from 'fivetwelve/es5';
6 |
7 | describe('DeviceRegistry', () => {
8 | let devices;
9 | beforeEach(() => {
10 | devices = [
11 | sinon.createStubInstance(DmxDevice),
12 | sinon.createStubInstance(DmxDevice),
13 | sinon.createStubInstance(DmxDevice),
14 | sinon.createStubInstance(DmxDevice),
15 | sinon.createStubInstance(DmxDevice),
16 | sinon.createStubInstance(DmxDevice)
17 | ];
18 | });
19 |
20 | it('can add and query elements', () => {
21 | let registry = new DeviceRegistry();
22 |
23 | registry.add(devices[0], 'dev0', ['spot', 'front', 'left']);
24 | registry.add(devices[1], 'dev1', ['spot', 'back', 'left']);
25 | registry.add(devices[2], 'dev2', ['spot', 'front', 'right']);
26 | registry.add(devices[3], 'dev3', ['spot', 'back', 'right']);
27 |
28 | let res = registry.select('.spot');
29 | expect(res).to.be.a(DeviceGroup);
30 | expect(res.devices).length(4);
31 |
32 | res = registry.select('.spot.left');
33 | expect(res.devices).length(2);
34 | expect(res.devices[0]).to.be(devices[0]);
35 | expect(res.devices[1]).to.be(devices[1]);
36 |
37 | res = registry.select('.spot.front.right');
38 | expect(res.devices).length(1);
39 | expect(res.devices[0]).to.be(devices[2]);
40 |
41 | res = registry.select('*');
42 | expect(res.devices).length(4);
43 |
44 | res = registry.getAll();
45 | expect(res.devices).length(4);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/wallaby.js:
--------------------------------------------------------------------------------
1 | var babel = require('babel');
2 |
3 | module.exports = function(wallaby) {
4 | return {
5 | files: [
6 | 'lib/**/*.js',
7 | 'node_modules/fivetwelve/{*.js,**/*.js}'
8 | ],
9 | tests: [
10 | 'test/**/*Test.js'
11 | ],
12 |
13 | compilers: {
14 | '**/*.js': wallaby.compilers.babel({
15 | babel: babel,
16 | stage: 0 // https://babeljs.io/docs/usage/experimental/
17 | })
18 | },
19 |
20 | env: {
21 | type: 'node',
22 | params: {
23 | //runner: '--harmony --harmony_arrow_functions',
24 | //env: 'PARAM1=true;PARAM2=false'
25 | }
26 | }
27 | };
28 | };
29 |
--------------------------------------------------------------------------------