Skip to content

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

@RubyLouvre

Description

@RubyLouvre

早在 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
        });

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions