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