├── .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 | --------------------------------------------------------------------------------