在重新审视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();
});
}());