├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── additive-transform.js
├── gulpfile.js
├── package.json
└── test.html
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | charset = utf-8
8 |
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Brett Jephson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | additive-transform-js
2 | =====================
3 |
4 | Small Javascript utility for CSS transforms.
5 |
6 | Based on my answer to this Stack Overflow question: http://stackoverflow.com/questions/20167239/how-to-mix-css3-transform-properties-without-overriding-in-realtime/20200863/#20200863.
7 |
8 | Takes a set of CSS transform rules and applies them to a node one by one - to create complicated transforms from simple rules. Rather than the CSS default behaviour of overriding previously applied transforms.
9 |
10 | For example:
11 |
12 | CSS:
13 | ```css
14 | .transform-1 {
15 | transform: scale(1.5, 1.5);
16 | }
17 | .transform-2 {
18 | transform: translate(5px, 5px);
19 | }
20 | .transform-3 {
21 | transform: rotate(90deg);
22 | }
23 | ```
24 |
25 | HTML:
26 | ```html
27 |
TEST CASE
28 | ```
29 |
30 | Without the Javascript, this would result in #test-case rotating 90 degrees. With the Javascript, #test-case is scaled, translated and then rotated.
31 |
32 | To use:
33 | ---------
34 |
35 | Simply add the script and call transform to run it:
36 |
37 | ```
38 |
39 |
42 | ```
43 |
44 | There is a configure function where you can change:
45 | * selector - default is '.add-transforms' - script looks for all instances of the selector on the page using document.querySelectorAll.
46 | * dataAttribute - default is 'data-transforms' - script applies all the transforms listed in this data attribute (must be a comma-separated list).
47 |
48 | ```
49 | AdditiveTransform.configure({ selector: "#my-transform", dataAttribute: "data-my-transform-list" });
50 | ```
--------------------------------------------------------------------------------
/additive-transform.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Brett on 26/11/13.
3 | */
4 | (function(exports,document,undefined){
5 | "use strict";
6 |
7 | var IDENTITY_MATRIX = {
8 | a: 1.0,
9 | b: 0.0,
10 | c: 0.0,
11 | d: 1.0,
12 | tx: 0.0,
13 | ty: 0.0
14 | };
15 |
16 | function forEachNode(nodes, action, thisArg) {
17 | var forEach = Array.prototype.forEach;
18 | forEach.call(nodes, action, thisArg || this);
19 | }
20 |
21 | function CSSParser() {
22 | var DEGREES_TO_RADIANS = Math.PI/180;
23 | var GRADIANS_TO_RADIANS = Math.PI/200;
24 | var TURNS_TO_RADIANS = 2*Math.PI;
25 |
26 | var REGEXP_ALL_LETTERS_AND_BRACKETS = /[a-zA-Z\(\)]/g;
27 |
28 | var REGEXP_MATRIX = /matrix/;
29 | var REGEXP_TRANSLATE = /translate[X|Y]?/;
30 | var REGEXP_SCALE = /scale[X|Y]?/;
31 | var REGEXP_ROTATE = /rotate/;
32 | var REGEXP_SKEW = /skew[X|Y]?/;
33 |
34 | var cachedTransformPropertyName = null;
35 |
36 | var isUndefined = function(value) {
37 | return typeof value === "undefined";
38 | };
39 |
40 | this.getSupportedTransform = function() {
41 | if( cachedTransformPropertyName ) { return cachedTransformPropertyName; }
42 | var transformPropertyName = null;
43 | var vendorTransformOptions = [
44 | 'transform',
45 | 'WebkitTransform',
46 | 'MozTransform',
47 | 'msTransform',
48 | 'Otransform'
49 | ];
50 | var dummy = document.createElement('div');
51 |
52 | vendorTransformOptions.some(function(element){
53 | if(!isUndefined(dummy.style[element])) {
54 | transformPropertyName = element;
55 | return true;
56 | }
57 | return false;
58 | });
59 | dummy = null;
60 | cachedTransformPropertyName = transformPropertyName;
61 | return cachedTransformPropertyName;
62 | };
63 |
64 | this.cssMatrixToObject = function(cssMatrix) {
65 | var matrixValues = cssMatrix.replace(/matrix/gi, "").replace(/[ \(\)]+/g,"").split(",");
66 | return {
67 | a: parseFloat( matrixValues[0] ),
68 | b: parseFloat( matrixValues[1] ),
69 | c: parseFloat( matrixValues[2] ),
70 | d: parseFloat( matrixValues[3] ),
71 | tx: parseFloat( matrixValues[4] ),
72 | ty: parseFloat( matrixValues[5] )
73 | };
74 | };
75 |
76 | this.matrixObjectToCSS = function(matrixObject){
77 | return "matrix("+
78 | matrixObject.a.toFixed(2)+
79 | ", "+matrixObject.b.toFixed(2)+
80 | ", "+matrixObject.c.toFixed(2)+
81 | ", "+matrixObject.d.toFixed(2)+
82 | ", "+matrixObject.tx.toFixed(2)+
83 | ", "+matrixObject.ty.toFixed(2)+")";
84 | };
85 |
86 | var parseCSSValue = function(value, numericValue, outputFormat, unitIdentifiers, isError, errorMessage){
87 | var result,
88 | unitIdentifier,
89 | item;
90 | for(unitIdentifier in unitIdentifiers) {
91 | if(unitIdentifiers.hasOwnProperty(unitIdentifier)) {
92 | item = unitIdentifiers[unitIdentifier];
93 | if(item.check(value)){
94 | result = item.parse[outputFormat].call(this, numericValue);
95 | break;
96 | }
97 | }
98 | }
99 | if(isError(result)){ throw errorMessage(value); }
100 | return result;
101 | };
102 |
103 | var parseRotateToRadians = function(value){
104 | var numericValues = value.replace( REGEXP_ALL_LETTERS_AND_BRACKETS, "").replace(/ /g, "").split(",").map(function(item){ return parseFloat(item); });
105 | return parseCSSValue(value, numericValues[0], "toRadians", angleUnitIdentifiers, isUndefined, function(value){
106 | return "Angle unit identifier not recognised for value: "+value;
107 | });
108 | };
109 |
110 | var parseScaleToXY = function(value){
111 | var numericValues = value.replace( REGEXP_ALL_LETTERS_AND_BRACKETS, "").replace(/ /g, "").split(",").map(function(item){ return parseFloat(item); });
112 | if(numericValues.length == 1) {
113 | if(/scaleX/.exec(value)) {
114 | numericValues[1] = 1.0;
115 | } else if (/scaleY/.exec(value)) {
116 | numericValues[1] = numericValues[0];
117 | numericValues[0] = 1.0;
118 | } else {
119 | numericValues[1] = numericValues[0];
120 | }
121 | }
122 | return {x: numericValues[0], y: numericValues[1]};
123 | };
124 |
125 | var parseTranslateToXY = function(value){
126 | var numericValues = value.replace( REGEXP_ALL_LETTERS_AND_BRACKETS, "").replace(/ /g, "").split(",").map(function(item){ return parseFloat(item); });
127 | var parsedNumericValues = numericValues.map(function(numericValue) {
128 | return parseCSSValue(
129 | value,
130 | numericValue,
131 | "toPixels",
132 | lengthUnitIdentifiers,
133 | isUndefined,
134 | function(value) {
135 | return "Translate unit identifier not recognised for value: " + value;
136 | }
137 | );
138 | });
139 |
140 | if(parsedNumericValues.length == 1) {
141 | if(/translateX/.exec(value)) {
142 | parsedNumericValues[1] = 0.0;
143 | } else if(/translateY/.exec(value)) {
144 | parsedNumericValues[1] = parsedNumericValues[0];
145 | parsedNumericValues[0] = 0.0;
146 | } else {
147 | parsedNumericValues[1] = 0.0;
148 | }
149 | }
150 |
151 | return {x: parsedNumericValues[0] , y: parsedNumericValues[1] };
152 | };
153 |
154 | var parseSkewToXY = function(value){
155 | var numericValues = value.replace( REGEXP_ALL_LETTERS_AND_BRACKETS, "").replace(/ /g, "").split(",").map(function(item){ return parseFloat(item); });
156 | var parsedNumericValues = numericValues.map(function(numericValue) {
157 | return parseCSSValue(
158 | value,
159 | numericValue,
160 | "toRadians",
161 | angleUnitIdentifiers,
162 | isUndefined,
163 | function(value){
164 | return "Skew unit identifier not recognised for value: "+value;
165 | }
166 | );
167 | });
168 |
169 | if(parsedNumericValues.length == 1)
170 | {
171 | if(/skewX/.exec(value)) {
172 | parsedNumericValues[1] = 0.0;
173 | } else if(/skewY/.exec(value)) {
174 | parsedNumericValues[1] = parsedNumericValues[0];
175 | parsedNumericValues[0] = 0.0;
176 | } else {
177 | parsedNumericValues[1] = 0.0;
178 | }
179 | }
180 |
181 | return { x: parsedNumericValues[0], y: parsedNumericValues[1] };
182 | };
183 |
184 | this.transformFunctions2D = {
185 | matrix: {
186 | check: function(testValue){
187 | return REGEXP_MATRIX.exec(testValue);
188 | },
189 | parse: function(value) {
190 |
191 | },
192 | transform: function(value, matrix){
193 |
194 | }
195 | },
196 | translate: {
197 | check: function(testValue){
198 | return REGEXP_TRANSLATE.exec(testValue);
199 | },
200 | parse: function(value){
201 | return parseTranslateToXY(value);
202 | },
203 | transform: function(value, matrix){
204 | matrix.tx += value.x;
205 | matrix.ty += value.y;
206 | return matrix;
207 | }
208 | },
209 | scale: {
210 | check: function(testValue){
211 | return REGEXP_SCALE.exec(testValue);
212 | },
213 | parse: function(value){
214 | return parseScaleToXY(value);
215 | },
216 | transform: function(value, matrix){
217 | matrix.a *= value.x;
218 | matrix.b *= value.y;
219 | matrix.c *= value.x;
220 | matrix.d *= value.y;
221 | matrix.tx *= value.x;
222 | matrix.ty *= value.y;
223 | return matrix;
224 | }
225 | },
226 | rotate: {
227 | check: function(testValue){
228 | return REGEXP_ROTATE.exec(testValue);
229 | },
230 | parse: function(value){
231 | return parseRotateToRadians(value);
232 | },
233 | transform: function(value, matrix) {
234 | var cosValue = Math.cos( value );
235 | var sinValue = Math.sin( value );
236 | var a = matrix.a;
237 | var b = matrix.b;
238 | var c = matrix.c;
239 | var d = matrix.d;
240 | var tx = matrix.tx;
241 | var ty = matrix.ty;
242 | matrix.a = a*cosValue - b*sinValue;
243 | matrix.b = a*sinValue + b*cosValue;
244 | matrix.c = c*cosValue - d*sinValue;
245 | matrix.d = c*sinValue + d*cosValue;
246 | matrix.tx = tx*cosValue - ty*sinValue;
247 | matrix.ty = tx*sinValue + ty*cosValue;
248 | return matrix;
249 | }
250 | },
251 | skew: {
252 | check: function(testValue){
253 | return REGEXP_SKEW.exec(testValue);
254 | },
255 | parse: function(value){
256 | return parseSkewToXY(value);
257 | },
258 | transform: function(value, matrix){
259 | matrix.b += Math.tan( value.y );
260 | matrix.c += Math.tan( value.x );
261 | return matrix;
262 | }
263 | }
264 | };
265 |
266 | ///TODO Add 3D transforms
267 | /**
268 | this.transformFunctions3D = {
269 |
270 | };
271 | **/
272 |
273 | var lengthUnitIdentifiers = {
274 | px: {
275 | check: function(testValue){
276 | return /px/.exec(testValue);
277 | },
278 | parse: {
279 | toPixels: function(value){
280 | return value;
281 | }
282 | }
283 | }
284 | };
285 |
286 | var angleUnitIdentifiers = {
287 | deg:{
288 | check: function(testValue){
289 | return /deg/.exec(testValue);
290 | },
291 | parse: {
292 | toRadians: function(value){
293 | return value * DEGREES_TO_RADIANS;
294 | }
295 | }
296 | },
297 | grad:{
298 | check: function(testValue){
299 | return /grad/.exec(testValue);
300 | },
301 | parse: {
302 | toRadians: function(value){
303 | return value * GRADIANS_TO_RADIANS;
304 | }
305 | }
306 | },
307 | rad:{
308 | check: function(testValue){
309 | return /(!g)rad/.exec(testValue);
310 | },
311 | parse: {
312 | toRadians: function(value){
313 | return value;
314 | }
315 | }
316 | },
317 | turn:{
318 | check: function(testValue){
319 | return /turn/.exec(testValue);
320 | },
321 | parse: {
322 | toRadians: function(value){
323 | return value * TURNS_TO_RADIANS;
324 | }
325 | }
326 | }
327 | };
328 | }
329 |
330 | var AdditiveTransform = {
331 | _config: {
332 | selector: ".add-transforms",
333 | dataAttribute: "data-transforms"
334 | },
335 | _cssParser: new CSSParser(),
336 | configure: function (config) {
337 | for (var property in this._config) {
338 | if (this._config.hasOwnProperty(property) && property in config) {
339 | this._config[property] = config[property];
340 | }
341 | }
342 | },
343 | transform: function () {
344 | var allNodesToTransform = document.querySelectorAll(this._config.selector);
345 | forEachNode(allNodesToTransform, this.transformNode, this);
346 | },
347 | transformNode: function (node) {
348 | var transformationSelectors = this.getNodeSelectors(node),
349 | nodeState = this.getNodeInitialState(node);
350 |
351 | transformationSelectors.forEach(function(transformationSelector) {
352 | nodeState = this.addTransform(node, nodeState, transformationSelector);
353 | }, this);
354 | },
355 | getNodeSelectors: function (node) {
356 | return node.getAttribute(this._config.dataAttribute).replace(/ /g, "").split(",");
357 | },
358 | getNodeInitialState: function (node) {
359 | var initialStateCSS = getComputedStyle(node, null)[this._cssParser.getSupportedTransform()];
360 | return (initialStateCSS === "none") ?
361 | IDENTITY_MATRIX :
362 | this._cssParser.cssMatrixToObject(initialStateCSS);
363 | },
364 | addTransform: function (node, nodeState, selector) {
365 | var cssTransformationRules = this.getCSSRulesFor(selector);
366 | cssTransformationRules.forEach(function(rule) {
367 | if (/transform/.exec(rule.property)) {
368 | var matrix = this.transformMatrix(nodeState, rule.value);
369 | node.style[this._cssParser.getSupportedTransform()] = this._cssParser.matrixObjectToCSS(matrix);
370 | nodeState = matrix;
371 | }
372 | }, this);
373 | return nodeState;
374 | },
375 | getCSSRulesFor: function (selector) {
376 | var cssRules = [];
377 | forEachNode(document.styleSheets, function(sheet) {
378 | if (!sheet.cssRules) {
379 | return;
380 | }
381 | forEachNode(sheet.cssRules, function(rule) {
382 | if (rule.selectorText && rule.selectorText.split(',').indexOf(selector) !== -1) {
383 | var styles = rule.style;
384 | forEachNode(rule.style, function(style) {
385 | cssRules.push({
386 | property: style,
387 | value: styles[style]
388 | });
389 | });
390 | }
391 | });
392 | });
393 | return cssRules;
394 | },
395 | transformMatrix: function (source, transform) {
396 | var values = transform.split(") "); // split into array if multiple values
397 | var matrix = Object.create(source);
398 |
399 | values.forEach(function (value) {
400 | var transformFunction;
401 | var transformFunctionName;
402 |
403 | for (transformFunctionName in this._cssParser.transformFunctions2D) {
404 | if (this._cssParser.transformFunctions2D.hasOwnProperty(transformFunctionName)) {
405 | transformFunction = this._cssParser.transformFunctions2D[transformFunctionName];
406 | if (transformFunction.check(value)) {
407 | var transformValue = transformFunction.parse(value);
408 | matrix = transformFunction.transform(transformValue, matrix);
409 | break;
410 | }
411 | }
412 | }
413 | }, this);
414 | return matrix;
415 | }
416 | };
417 |
418 | exports.AdditiveTransform = exports.AdditiveTransform || AdditiveTransform;
419 | })(window,document);
420 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var jshint = require('gulp-jshint');
3 | var stylish = require('jshint-stylish');
4 |
5 | gulp.task('lint', function() {
6 | return gulp.src('./*.js')
7 | .pipe(jshint())
8 | .pipe(jshint.reporter(stylish));
9 | });
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "additive-transform-js",
3 | "version": "1.0.0",
4 | "description": "Javascript utility for composite CSS transforms",
5 | "main": "additive-transform.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/brejep/additive-transform-js"
12 | },
13 | "keywords": [
14 | "Javascript",
15 | "CSS",
16 | "transforms"
17 | ],
18 | "author": "Brett Jephson",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/brejep/additive-transform-js/issues"
22 | },
23 | "homepage": "https://github.com/brejep/additive-transform-js",
24 | "devDependencies": {
25 | "gulp": "^3.9.0",
26 | "gulp-jshint": "^1.11.2",
27 | "jshint-stylish": "^2.0.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test Additive Transform
6 |
56 |
57 |
58 | TEST CASE
59 | TEST CASE 2
60 |
61 |
64 |
65 |
--------------------------------------------------------------------------------