Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

css3 transform 2D的支持改进与实现 #9

Open
RubyLouvre opened this issue May 9, 2012 · 0 comments
Open

css3 transform 2D的支持改进与实现 #9

RubyLouvre opened this issue May 9, 2012 · 0 comments

Comments

@RubyLouvre
Copy link
Owner

早在 v2时,它就加入了对旋转与放大的支持,但由于数学知识的短板一直搞出个矩阵类。而矩阵类是解决IE transform 2D的关键。从上星期决定升级CSS模块开始,就想方设法引进一个矩进类。 个人一开始很看重两个框架 Sylvester.js ,与matrix.js。但它们都太大了,最终还是决定自己搞:

function toFixed(d){
    return  d > -0.0000001 && d < 0.0000001 ? 0 : /e/.test(d+"") ? d.toFixed(7) : d
}
function rad(value) {
    if(isFinite(value)) {
        return parseFloat(value);
    }
    if(~value.indexOf("deg")) {//圆角制。
        return parseInt(value,10) * (Math.PI / 180);
    } else if (~value.indexOf("grad")) {//梯度制。一个直角的100等分之一。一个圆圈相当于400grad。
        return parseInt(value,10) * (Math.PI/200);
    }//弧度制,360=2π
    return parseFloat(value,10)
}
var Matrix = $.factory({
    init: function(rows,cols){
        this.rows = rows || 3;
        this.cols = cols || 3;
        this.set.apply(this, [].slice.call(arguments,2))
    },
    set: function(){//用于设置元素
        for(var i = 0, n = this.rows * this.cols; i < n; i++){
            this[ Math.floor(i / this.rows) +","+(i % this.rows) ] = parseFloat(arguments[i]) || 0;
        }
        return this;
    },
    get: function(){//转变成数组
        var array = [], ret = []
        for(var key in this){
            if(~key.indexOf(",")){
                array.push( key )
            }
        }
        array.sort() ;
        for(var i = 0; i < array.length; i++){
            ret[i] = this[array[i]]
        }
        return  ret ;
    },
    set2D: function(a,b,c,d,tx,ty){
        this.a =  this["0,0"] = a * 1
        this.b =  this["1,0"] = b * 1
        this.c =  this["0,1"] = c * 1
        this.d =  this["1,1"] = d * 1
        this.tx = this["2,0"] = tx * 1
        this.ty = this["2,1"] = ty * 1
        this["0,2"] = this["1,2"] = 0
        this["2,2"] = 1;
        return this;
    },
    get2D: function(){
        return "matrix("+[ this["0,0"],this["1,0"],this["0,1"],this["1,1"],this["2,0"],this["2,1"] ]+")";
    },
    cross: function(matrix){
        if(this.cols === matrix.rows){
            var ret = new Matrix(this.rows, matrix.cols);
            var n = Math.max(this.rows, matrix.cols)
            for(var key in ret){
                if(key.match(/(\d+),(\d+)/)){
                    var r = RegExp.$1, c = RegExp.$2
                    for(var i = 0; i < n; i++ ){
                        ret[key] += ( (this[r+","+i] || 0) * (matrix[i+","+c]||0 ));//X轴*Y轴
                    }
                }
            }
            for(key in this){
                if(typeof this[key] == "number"){
                    delete this[key]
                }
            }
            for(key in ret){
                if(typeof ret[key] == "number"){
                    this[key] = toFixed(ret[key])
                }
            }
            return this
        }else{
            throw "cross error: this.cols !== matrix.rows"
        }
    },
    //http://www.zweigmedia.com/RealWorld/tutorialsf1/frames3_2.html
    //http://www.w3.org/TR/SVG/coords.html#RotationDefined
    //http://www.mathamazement.com/Lessons/Pre-Calculus/08_Matrices-and-Determinants/coordinate-transformation-matrices.html
    translate: function(tx, ty) {
        tx = parseFloat(tx) || 0;//沿 x 轴平移每个点的距离。
        ty = parseFloat(ty) || 0;//沿 y 轴平移每个点的距离。
        var m = (new Matrix()).set2D(1 ,0, 0, 1, tx, ty);
        this.cross(m)
    },
    translateX: function(tx) {
        this.translate(tx, 0)
    },
    translateY: function(ty) {
        this.translate(0, ty)
    },
    scale: function(sx, sy){
        sx = isFinite(sx) ? parseFloat(sx) : 1 ;
        sy = isFinite(sy) ? parseFloat(sy) : 1 ;
        var m = (new Matrix()).set2D( sx, 0, 0, sy, 0, 0);
        this.cross(m)
    },
    scaleX: function(sx) {
        this.scale(sx, 1)
    },
    scaleY: function(sy) {
        this.scale(1, sy)
    },
    rotate: function(angle, fix){//matrix.rotate(60)==>顺时针转60度
        fix = fix === -1 ? fix : 1;
        angle = rad(angle);
        var cos = Math.cos(angle);
        var sin = Math.sin(angle);// a, b, c, d
        var m = (new Matrix()).set2D( cos,fix * sin , fix * -sin, cos, 0, 0);
        return this.cross(m)
    },
    skew: function(ax, ay){
        var xRad = rad(ax);
        var yRad;
 
        if (ay != null) {
            yRad = rad(ay)
        } else {
            yRad = xRad
        }
        var m = (new Matrix()).set2D( 1, Math.tan( xRad ), Math.tan( yRad ), 1, 0, 0);
        return this.cross(m)
    },
    skewX: function(ax){
        return this.skew(ax, 0);
    },
    skewY: function(ay){
        this.skew(0, ay);
    },
 
    // ┌       ┐┌              ┐
    // │ a c tx││  M11  -M12 tx│
    // │ b d ty││  -M21  M22 tx│
    // └       ┘└              ┘
    //http://help.adobe.com/zh_CN/FlashPlatform/reference/actionscript/3/flash/geom/Matrix.html
    //分解原始数值,得到a,b,c,e,tx,ty属性,以及返回一个包含x,y,scaleX,scaleY,skewX,skewY,rotation的对象
    decompose2D: function(){
        var ret = {}
        this.a = this["0,0"]
        this.b = this["1,0"]
        this.c = this["0,1"]
        this.d = this["1,1"]
        ret.x = this.tx = this["2,0"]
        ret.y = this.ty = this["2,1"]
 
        ret.scaleX = Math.sqrt(this.a * this.a + this.b * this.b);
        ret.scaleY = Math.sqrt(this.c * this.c + this.d * this.d);
 
        var skewX = Math.atan2(-this.c, this.d);
        var skewY = Math.atan2(this.b, this.a);
 
        if (skewX == skewY) {
            ret.rotation = skewY/Matrix.DEG_TO_RAD;
            if (this.a < 0 && this.d >= 0) {
                ret.rotation += (ret.rotation <= 0) ? 180 : -180;
            }
            ret.skewX = ret.skewY = 0;
        } else {
            ret.skewX = skewX/Matrix.DEG_TO_RAD;
            ret.skewY = skewY/Matrix.DEG_TO_RAD;
        }
        return ret;
    }
});
"translateX,translateY,scaleX,scaleY,skewX,skewY".replace($.rword, function(n){
    Matrix.prototype[n.toLowerCase()] = Matrix.prototype[n]
});
Matrix.DEG_TO_RAD = Math.PI/180;

从这个矩阵类也可以看到,乘法是最重要的,什么translate, scale, skew, rotate都是基于它。唯一不爽的是,它的元素命名有点复杂。当然这是基于乘法运算的需要。由于野心太大,既可以实现2D矩阵,也可以实现3D矩阵,4*3矩阵……在实现过程中,得知矩阵相乘还是有条件的,于是理想主义死亡了。

第二版的矩阵类很简单,就是专攻2D,名字也从$.Matrix收窄为$.Matrix2D。放弃"x,y"这样复杂的元素命名法,改用a, b, c, d, tx, ty命名。基于cross的各种API也自行代码防御与数字转换,容错性大大提高!

    function toFixed(d){//矩阵类第二版
        return  d > -0.0000001 && d < 0.0000001 ? 0 : /e/.test(d+"") ? d.toFixed(7) : d
    }
     function toFloat(d, x){
        return isFinite(d) ? d: parseFloat(d) || x || 0
    }
    //http://zh.wikipedia.org/wiki/%E7%9F%A9%E9%98%B5
    //http://help.dottoro.com/lcebdggm.php
    var Matrix2D = $.factory({
        init: function(){
            this.set.apply(this, arguments);
        },
        cross: function(a, b, c, d, tx, ty) {
            var a1 = this.a;
            var b1 = this.b;
            var c1 = this.c;
            var d1 = this.d;
            this.a  = toFixed(a*a1+b*c1);
            this.b  = toFixed(a*b1+b*d1);
            this.c  = toFixed(c*a1+d*c1);
            this.d  = toFixed(c*b1+d*d1);
            this.tx = toFixed(tx*a1+ty*c1+this.tx);
            this.ty = toFixed(tx*b1+ty*d1+this.ty);
            return this;
        },
        rotate: function( radian ) {
            var cos = Math.cos(radian);
            var sin = Math.sin(radian);
            return this.cross(cos,  sin,  -sin, cos, 0, 0)
        },
        skew: function(sx, sy) {
            return this.cross(1, Math.tan( sy ), Math.tan( sx ), 1, 0, 0);
        },
        skewX: function(radian){
            return this.skew(radian, 0);
        },
        skewY: function(radian){
            return this.skew(0, radian);
        },
        scale: function(x, y) {
            return this.cross( toFloat(x, 1) ,0, 0, toFloat(y, 1), 0, 0)
        },
        scaleX: function(x){
            return this.scale(x ,1);
        },
        scaleY: function(y){
            return this.scale(1 ,y);
        },
        translate : function(x, y) {
            return this.cross(1, 0, 0, 1, toFloat(x, 0), toFloat(x, 0) );
        },
        translateX: function(x) {
            return this.translate(x, 0);
        },
        translateY: function(y) {
            return this.translate(0, y);
        },
        toString: function(){
            return "matrix("+this.get()+")";
        },
        get: function(){
            return [this.a,this.b,this.c,this.d,this.tx,this.ty];
        },
        set: function(a, b, c, d, tx, ty){
            this.a = a * 1;
            this.b = b * 1 || 0;
            this.c = c * 1 || 0;
            this.d = d * 1;
            this.tx = tx * 1 || 0;
            this.ty = ty * 1 || 0;
            return this;
        },
        matrix:function(a, b, c, d, tx, ty){
            return this.cross(a, b, c, d, toFloat(tx), toFloat(ty))
        },
        decompose : function() {
            //分解原始数值,返回一个包含x,y,scaleX,scaleY,skewX,skewY,rotation的对象
            var ret = {};
            ret.x = this.tx;
            ret.y = this.ty;
            ret.scaleX = Math.sqrt(this.a * this.a + this.b * this.b);
            ret.scaleY = Math.sqrt(this.c * this.c + this.d * this.d);

            var skewX = Math.atan2(-this.c, this.d);
            var skewY = Math.atan2(this.b, this.a);

            if (skewX == skewY) {
                ret.rotation = skewY/ Math.PI * 180;
                if (this.a < 0 && this.d >= 0) {
                    ret.rotation += (ret.rotation <= 0) ? 180 : -180;
                }
                ret.skewX = ret.skewY = 0;
            } else {
                ret.skewX = skewX/ Math.PI * 180;
                ret.skewY = skewY/ Math.PI * 180;
            }
            return ret;
        }
    });

    $.Matrix2D = Matrix2D

第二版与初版唯一没有动的地方是decompose 方法,这是从EaselJS抄过来的。而它的作用与louisremi的jquery.transform2的dunmatrix作用相仿,相后者据说是从FireFox源码从扒出来的,但EaselJS的实现明显顺眼多了。至于矩阵类的其他部分,则是从jQuery作者 John Resig的另一个项目Processing.js,不过它的位移与放缩部分有点偷懒,导致错误,于是外围API统统调用cross方法。

但光是有矩阵类是不行的,因此DOM的实现开始时是借鉴useragentman的这篇文章,追根索底,他也是参考另一位大牛的实现。 heygrady 在写了一篇叫《Correcting Transform Origin and Translate in IE》,阐述解题步骤。这些思路后来就被useragentman与louisremi 借鉴去了。但他们俩都在取得变形前元素的尺寸上遇到麻烦,为此使用了矩阵乘向量,然后取四个最上最下最左最右的坐标来求宽高,如此复杂的计算导致误差。因此我框架的CSS模块 v3唯一可做,也唯一能骄傲之处,就是给出更便捷更优雅的求变形前元素的尺寸的解。

下面就是css_fix有关矩阵变换的所有代码,可以看出,数据缓存系统非常重要!

    var ident  = "DXImageTransform.Microsoft.Matrix"

    adapter[ "transform:get" ] = function(node, name){
        var m = $._data(node,"matrix")
        if(!m){
            if(!node.currentStyle.hasLayout){
                node.style.zoom = 1;
            }
            //IE9下请千万别设置  <meta content="IE=8" http-equiv="X-UA-Compatible"/>
            //http://www.cnblogs.com/Libra/archive/2009/03/24/1420731.html
            if(!node.filters[ident]){
                var old = node.currentStyle.filter;//防止覆盖已有的滤镜
                node.style.filter =  (old ? old +"," : "") + " progid:" + ident + "(sizingMethod='auto expand')";
            }
            var f = node.filters[ident];
            m = new $.Matrix2D( f.M11, f.M12, f.M21, f.M22, f.Dx, f.Dy);
            $._data(node,"matrix",m ) //保存到缓存系统,省得每次都计算
        }
        return name === true ? m : m.toString();
    }
    //deg   degrees, 角度
    //grad  grads, 百分度
    //rad   radians, 弧度
    function toRadian(value) {
        return ~value.indexOf("deg") ?
        parseInt(value,10) *  Math.PI/180:
        ~value.indexOf("grad") ?
        parseInt(value,10) * Math.PI/200:
        parseFloat(value);
    }
    adapter[ "transform:set" ] = function(node, name, value){
        var m = adapter[ "transform:get" ](node, true)
        //注意:IE滤镜和其他浏览器定义的角度方向相反
        value.toLowerCase().replace(rtransform,function(_,method,array){
            array = array.replace(/px/g,"").match($.rword) || [];
            if(/skew|rotate/.test(method)){//角度必须带单位
                array[0] = toRadian(array[0] );//IE矩阵滤镜的方向是相反的
                array[1] = toRadian(array[1] || "0");
            }
            if(method == "scale" && array[1] == void 0){
                array[1] = array[0] //sy如果没有定义等于sx
            }
            if(method !== "matrix"){
                method = method.replace(/(x|y)$/i,function(_,b){
                    return  b.toUpperCase();//处理translateX translateY scaleX scaleY skewX skewY等大小写问题
                })
            }
            m[method].apply(m, array);
            var filter = node.filters[ident];
            filter.M11 =  filter.M22 = 1;//取得未变形前的宽高
            filter.M12 =  filter.M21 = 0;
            var width = node.offsetWidth;
            var height = node.offsetHeight;
            filter.M11 = m.a;
            filter.M12 = m.c;//★★★注意这里的顺序
            filter.M21 = m.b;
            filter.M22 = m.d;
            filter.Dx  = m.tx;
            filter.Dy  = m.ty;
            $._data(node,"matrix",m);
            var tw =  node.offsetWidth, th = node.offsetHeight;//取得变形后高宽
            node.style.position = "relative";
            node.style.left = (width - tw)/2  + m.tx + "px";
            node.style.top = (height - th)/2  + m.ty + "px";
          //http://extremelysatisfactorytotalitarianism.com/blog/?p=922
        //http://someguynameddylan.com/lab/transform-origin-in-internet-explorer.php
        //http://extremelysatisfactorytotalitarianism.com/blog/?p=1002
        });

注释里有许多链接,是向先行者致敬的!

在这过程中,还发现许多好东西,一并放出来,以供未来的偷师与转化!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant