CSS3 transform matrix 矩阵研究

在重新审视CSS3的时候发现一个优秀的专门介绍transform里matrix的demo:

http://peterned.home.xs4all.nl/matrices/


整体分为几个部分:


帮助函数

play:添加事件监听函数

Play.prototype:处理drag and drop,根据转换属性调用不同的matrix函数,display和current轮流切换来记住拖拽之前和之后的状态,matrix = matrix.xxx用来记住矩阵的状态

Vector:帮助矩阵运算

Matrix:矩阵运算函数

Style: 设置实际的css,包括显示最终结果的css


/**
 * Matrix Play
 *
 * Drag a box around to get the transform css!
 * Version 1.3 - 2012/03/03
 *
 */
(function() {

	
	// decimals of the matrix css output
	var FIXED = 3; 

	/**
	 * Various helpers
	 *
	 */

	if(!Function.prototype.bind) {
		Function.prototype.bind = function(scope) {
			var method = this;
			return function() {
				return method.apply(scope, arguments);
			};
		};
	}

	function find(selector) {
		return document.querySelectorAll(selector);
	}

	function closest(nodeName, node) {
		var reg = new RegExp('^' + nodeName + '$', 'i');
		while (node && !reg.test(node.nodeName)) {
			node = node.parentNode;
		}
		return node;
	}
	
	function addEvent(node, type, listener) {
		node.addEventListener(type, listener, false);
	}
	
	function addEvents(nodes, type, listener) {
		var i, l = nodes.length;
		for (i = 0; i < l; i++) {	
			addEvent(nodes[i], type, listener);
		}
	}


	/**
	 * Play
	 * 
	 */
	var Play = function() {
		this.play = find('#play')[0];
		this.out = find('#output')[0];
		this.mirrored = !/(peterned|xs4all)/.test(window.location.href);

		// the draggable handles
		var handles = find('#play span');
		var touch = Client.touchEnabled();
		//添加event handler,为了更好的用户体验,move和up事件都对document
		addEvents(handles, touch ? 'touchstart' : 'mousedown', 
			this.handleMousedown.bind(this));
		
		addEvent(document, touch ? 'touchmove' : 'mousemove', 
			this.handleMousemove.bind(this));

		addEvent(document, touch ? 'touchend' : 'mouseup', 
			this.handleMouseup.bind(this));
		
		addEvent(window, 'hashchange', this.restore.bind(this));

		// transformation mode (handle classname)
		this.mode = null; 
		
		// hash state
		this.state = 'none'; 
		
		// permanent transformation
		this.display = new Matrix();

		// temp transformation during move, added to display on release
		this.current = new Matrix();

		// if there is a usable hash, use it!
		if(window.location.hash) {
			this.restore();
		}
	};
	
	Play.prototype = {
		/**
		 * mousedown/touchstart handler
		 *
		 */
		handleMousedown: function(e){
			e.preventDefault();
			
			// jquery wraps the native event, need it for e.touches though
			var ev = this.getTouch(e);

			var handle = closest('span', e.target);
			var play = this.play;

			// store some vectors (see further below) for move calcs
			this.center = new Vector(
				play.offsetLeft + play.offsetWidth / 2, 
				play.offsetTop + play.offsetHeight / 2
			);
			console.info(ev.clientX, ev.clientY)
			this.anchor = new Vector(ev.clientX, ev.clientY);
			//鼠标和方块之间的位差			
			this.offset = this.anchor.subtract(this.center);

			this.mode = handle.className;
		},
		
		/**
		 * mousemove/touchmove handler
		 *
		 */
		handleMousemove: function(e){
			
			// If no current mode, do nothing. Conditional binding would be prettier.
			if(!this.mode) {
				return;
			}

			e.preventDefault();

			var ev = this.getTouch(e);
			
			var point = new Vector(ev.clientX, ev.clientY);
			var center = this.center;
			var anchor = this.anchor;
			var offset = this.offset;
			var matrix = this.display;

			/**
			 * transform the current matrix based on the mode (classname) of the dragged handle
			 *
			 * <3 math <3
			 */
			switch (this.mode) {
				case 'rotate':
					var moved = point.subtract(anchor);
					var y = moved.y;
					var x = point.subtract(center).x;
					var r = Math.atan2(y, x);
					matrix = matrix.rotate(r);
				break;

				case 'scale':
					var moved = point.subtract(center);
					var scaled = moved.divide(offset);
					matrix = matrix.scale(scaled);
				break;

				case 'move':
					var moved = point.subtract(anchor);
					console.info(moved)
					matrix = matrix.translate(moved);
				break;

				case 'skewx':
					var moved = point.subtract(anchor);
					matrix = matrix.skew(
						new Vector(moved.x/offset.y, 0)
					);
				break;

				case 'skewy':
					var moved = point.subtract(anchor);
					matrix = matrix.skew(
						new Vector(0, moved.y/offset.x)
					);
				break;
			}

			// store and show the current transformation
			this.current = matrix;
			this.transform(matrix);
		},

		handleMouseup: function(e){
			// yes. I know
			if(!this.mode) {
				return;
			}

			e.preventDefault();
			
			// clear the mode, update display and output result.
			this.mode = null;
			this.save();
		},

		getTouch: function(e) {
			// multitouch vs mouse
			return e.touches? e.touches[0] : e;
		},
		
		/**
		 * send the matrix over to the Style object (see below)
		 *
		 */
		transform: function(matrix) {
			Style.setTransform(this.play, matrix);
		},

		save: function() {
			this.display = this.current;

			// yeah this looks terrible
			var state = this.current.toArray().map(function(n) {
				return (parseInt(n, 10) !== n)? n.toFixed(FIXED) : n;
			}).join(',');

			
			this.render(this.display);
			this.state = state;

			if(!this.mirrored) {
				window.location.hash = state;
			}

		},

		reset: function() {
			var reset = new Matrix();
			this.display = reset;
			this.transform(reset);
			this.render(reset);
			this.state = null;
		},
		
		/**
		 * Outputs the given matrix to the output area for copy-pasting
		 *
		 */
		render: function(matrix) {
			var css = Style.toCSS(matrix, FIXED);
			this.out.innerHTML = css;
		},

		/**
		 * attempt to restore the display to the given hash
		 *
		 */
		restore: function() {
			var hash = window.location.hash;			
			if(hash && hash.length) {
				try {
					var state = hash.substring(1);
					if(state === this.state) {
						// same, skip
						return;
					}

					var input = state.split(',').map(parseFloat);
					if(input.length == 6) {
						var matrix = Matrix.parseArray(input);
						this.display = matrix;
						this.transform(matrix);
						this.render(matrix);
						this.state = state;
					}
				} catch (e) {
					// something went wrong, let's reset it
					this.reset();
				}
			} else {
				// back to initial pageload, no hash
				this.reset();
			}
		}
	};


	/**
	 * 2D vector for prettuer 2D <3 math <3
	 *
	 */
	var Vector = function(x, y) {
		this.x = x;
		this.y = y;
	};

	Vector.prototype = {
		add: function(v) {
			return new Vector(this.x + v.x, this.y + v.y);
		},
		
		subtract: function(v) {
			return new Vector(this.x - v.x, this.y - v.y);
		},
		
		multiply: function(v) {
			return new Vector(this.x * v.x, this.y * v.y);
		},
		
		divide: function(v) {
			return new Vector(this.x / v.x, this.y / v.y);
		},
		
		// not used
		dot:function(b){
			var d = this.multiply(b);
			return (d.x + d.y);
		},

		// not used
		magnitude:function() {
			var x = this.x;
			var y = this.y;
			return _sqrt(x*x + y*y);
		},

		// not used
		normalize: function() {
			var m = this.magnitude();
			return new Vector(this.x/m, this.y/m);
		},
		
		// applies a matrix to this vector for total transformation, not used in this app.
		transform: function(matrix) {
			var m = matrix.base;
			return new Vector(
				m[0] * this.x + m[1] * this.y + m[2], 
				m[3] * this.x + m[4] * this.y + m[5]
			);
		}
	};


	/**
	 * Matrix, actually a 3x3 one, but the bottom line would always say 0 0 1, which would cause lots of senseless 0's to be calculated everywhere.
	 *
	 */
	var Matrix = function(base) {
		this.base = base || [1, 0, 0, 0, 1, 0];
	};

	Matrix.prototype = {
		_multiply: function(
			a1,a2,a3,a4,a5,a6, 
			b1,b2,b3,b4,b5,b6
		) {
			return new Matrix([
				a1 * b1 + a2 * b4,
				a1 * b2 + a2 * b5,
				a1 * b3 + a2 * b6 + a3, // because of the 0 0 1
				a4 * b1 + a5 * b4,
				a4 * b2 + a5 * b5,
				a4 * b3 + a5 * b6 + a6 // and again
			]);
		},

		multiply:function(b) {
			// apply the concatenated arrays to get 12 arguments, is actually faster.
			return this._multiply.apply(this, this.base.concat(b));
		},

		translate:function(v) {
			return this.multiply([1, 0, v.x, 0, 1, v.y]);
		},

		rotate:function(r) {
			var sin = Math.sin(r);
			var cos = Math.cos(r);
			return this.multiply([cos, sin, 0, -sin, cos, 0]);
		},

		scale:function(v) {
			return this.multiply([v.x, 0, 0, 0, v.y, 0]);
		},

		skew: function(v) {
			return this.multiply([1, v.y || 0, 0, v.x || 0, 1, 0]);
		},

		// CSS puts the translation elsewhere, really a 2x2 matrix with added dx and dy instead of a 3x3
		toArray: function() {
			var b = this.base;
			return [b[0], b[1], b[3], b[4], b[2], b[5]];
		}
	};

	Matrix.parseArray = function(b) {
		// 2x2 vs 3x3
		return new Matrix([
			b[0], b[1], b[4],
			b[2], b[3], b[5]
		]);
	};


	/**
	 * Tiny client check
	 *
	 */
	var ua = navigator.userAgent;
	var Client = {
		touchEnabled: function() {
			return !!('ontouchstart' in window);
		},
		
		webkit: /webkit\//i.test(ua),
		gecko: /gecko\//i.test(ua),
		trident: /trident\//i.test(ua),
		presto: /presto\//i.test(ua)
	};

	
	/**
	 * Style
	 *
	 */
	var Style = (function() {
	
		var prefix = 
			Client.webkit ? '-webkit-' : 
			Client.gecko ? '-moz-' :
			Client.presto ? '-o-' : 
			Client.trident ? '-ms-' : '';

		var csstransition = prefix + 'transition';
		var csstransform = prefix + 'transform';
		var regtransform = /(transform)/g;
		var regreplace = prefix + '$1';

		var PRIORITY = ''; // eg !important

		var unit = function(val) {
			return (typeof val === 'number')? (val + 'px') : val;
		};

		return {
			// not used
			setTransition: function(node, transition) {
				var value = transition.replace(regtransform, regreplace);
				this.set(node, csstransition, value);
			},
			
			setTransform: function(node, transform) {
				var value = transform;
				if(transform instanceof Matrix) {
					var array = transform.toArray();
					
					// mozilla requires units in the matrix's translation
					if(Client.gecko) {
						array[4] += 'px';
						array[5] += 'px';
					}
					
					value = 'matrix(' + array.join(',') + ')';
				}
				
				this.set(node, csstransform, value);
			},
			
			// not used
			setTranslate: function(node, x, y) {
				this.set(node, csstransform, 
					'translate(' + unit(x) + ',' + unit(y) + ')');
			},

			set: 
				Client.trident? // IE
				
				function(node, property, value) {
					node.style[property] = value;
				} : 

				function(node, property, value) {
					node.style.setProperty(property, value, PRIORITY);
				},

			// not used
			get: function(node, property) {
				var computed = window.getComputedStyle(node, null);
				var style = node.style;
				var value = 
					computed.getPropertyValue(property) || 
					style.getPropertyValue(property) ||
					style[property];

				return value;
			},

			/**
			 * get the CSS of the given matrix
			 *
			 */
			toCSS: function(matrix, fix) {
				var cleaned = matrix.toArray().map(function(n){
					return (fix && (parseInt(n, 10) !== n)) ? n.toFixed(fix) : n
				});
				
				var css = cleaned.join(',');
				cleaned[4] += 'px';
				cleaned[5] += 'px';				
				var moz = cleaned.join(',');
				
				return [
					'-webkit-transform: matrix(' + css + ');',
					'-moz-transform: matrix(' + moz + ');',
					'-ms-transform: matrix(' + css + ');',
					'-o-transform: matrix(' + css + ');',
					'transform: matrix(' + css + ');'
				].join('\n');
			}
		};

	}());


	/**
	 * Run!
	 * 
	 */

	addEvent(window, 'DOMContentLoaded', function () {
		var matrix = new Play();
	});

}());


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值