首頁  >  文章  >  web前端  >  HTML5邊玩邊學(六)-汽車人,變形的詳細介紹

HTML5邊玩邊學(六)-汽車人,變形的詳細介紹

黄舟
黄舟原創
2017-03-29 15:07:211673瀏覽

一、狀態及其保存與恢復

在這一節開始之前,我們要先理解一下什麼是狀態以及狀態的保存與復原。玩過MFC 程式的人常常可以碰到這樣的程式碼:

pOldPen=pDC->SelectObject(pNewPen)

我們在選擇新畫筆物件的同時,總是要儲存住舊畫筆對象,為什麼要這樣做呢?因為新畫筆物件只是臨時用一下,等用完了,我們想恢復到原來的畫筆配置時,如果舊的配置事先沒有被保存,這些配置就丟失了,也就沒辦法恢復了。

在HTML5 繪圖中,某一刻的狀態就是當前這一刻上下文物件的一系列屬性的配置值,只是,決定一個畫筆狀態的屬性比較少,如顏色、粗細、線型之類的,而確定上下文狀態的屬性比較多,包括下面這些:

1、當前上下文物件的移動、旋轉、縮放配置

2、當前上下文物件的strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 屬性值

#3、目前上下文物件的裁切路徑配置

上面這一系列配置決定了目前這一時刻上下文物件的狀態,其中移動、旋轉、縮放、globalCompositeOperation(組合)、裁剪下面我們馬上會講到。

二、狀態的保存與恢復

上面我們說某一刻的狀態由那麼多屬性決定,我們要保存這一刻的狀態就要把這些屬性值一個一個都保存,恢復的時候在一個一個都設定回去,那也太麻煩了。確實是這樣的,所以上下文對下提供了兩個簡單的方法,對狀態進行保存和恢復,他們是:

save( ) 和restore( )

save 和restore方法可以多次調用,每調用一次save 方法,調用時的狀態(即一系列屬性值)就會壓入一個堆疊中。

每呼叫一次 restore 方法,最後一次 save 的狀態就會恢復,也就是出棧。

想像一下彈匣,第一顆被發射出去的子彈,總是最後一個被壓入彈匣的。

 

三、變型

1、移動:translate(dx,dy)

這個方法看起來很簡單,其實它包含了一定的數學意義,你可以認為是整個座標系的原點發生了移動,新座標系下任意一點(x,y)相當於原座標系下的座標為:

x'=x+dx
y'=y+dy

如果我們呼叫ctx.translate(5,8) 改變上下文物件的座標系狀態,然後在新狀態下的點(3,2)繪圖,相當於圖像被繪製到了原始狀態下的點(8,10)處,即

x'=5+3=8
y'=5+ 2=10

也許你會問,為什麼要那麼麻煩,直接在(8,10)畫比行嗎?例如把

ctx.translate(5,8)
ctx.drawImage(img,3,2)

改成

ctx.drawImage(img,8 ,10)

這樣不是更簡單、更直接嗎?

我的理解是,移動更多的情況下是為其他圖形變換服務的,恰當的改變坐標原點可以讓圖形學計算更好理解,並帶來很大方便,下面我舉個簡單的例子,假如:

有一條線段,是x 軸正向上的一小段

y = 0 (1 <= x <= 3),

#若以座標原點為圓心,逆時針旋轉90度,則線段與y 軸正向重合,旋轉後的線段為:

x = 0 (1 <= y <= 3)

但是我們不可能每次旋轉都以原點為圓心進行旋轉,假如我們以線段的一個端點(1,0)為圓心進行旋轉,我們怎麼才能得到旋轉後線段上每一點的坐標值呢?其實這個過程可以分成三個步驟:

第一步:移動原點座標到(1,0),新的線段依然在x 軸上,但方程式變成:y = 0 (0 <= x <= 2)

第二步:以新座標系的原點為圓心進行旋轉,得到新座標系下的線段x = 0 (0 <= y <= 2)

第三步:將新座標系的原點移動到新座標系下(-1,0)處,即將原點恢復到原來的位置,此時的線段為:x = 1 (0 <= y <= 2)

第三步驟所得到的線段就是最後需要繪製的線段。

從這個例子我們可以看出來,即使在這麼簡單的情況下,如果不移動座標原點來直接計算旋轉後的圖形,也是比較困難的。

提示當你移動座標原點之前,千萬別忘了保存狀態,當然,繪製完畢後也別放了恢復狀態。

2、縮放scale(sx, sy)

這個也很簡單,sx, sy 是縮放比例因子,縮放後新座標系下任一點(x,y) 相當於原座標系下的座標為:

x' = x * sx
y' = y * sy

同樣,改變座標系統總是不要忘記儲存和復原狀態

3、旋轉rotate(A)

angle 是旋轉角度,旋轉後新座標系下任一點(x,y) 相當於原座標系下的座標為:

x' = x cosA - y sinA
y' = x sinA + y cosA

同樣,改變座標系統總是不要忘記保存和恢復狀態

4、變形transform(m11, m12, m21, m22, dx, dy)

其實前面講的移動、縮放、旋轉都是變形的特例,transform 方法的六個參數組成了一個變形矩陣,如下

m11 m21 dx
#m12 m22
# #######0######0######1#############

呼叫transform 方法就相當於用這些參數為上下文物件設定了新的變形矩陣,關於變形矩陣的具體內容可以參考圖形學相關資料,以下給出幾個簡單特例:

移動translate(dx,dy):相當於transform(1,0,0,1,dx,dy)

縮放scale(sx,xy):相當於transform(sx,0,0,sy,0,0)

#旋轉rotate(A) :相當於transform(cosA,sinA,-sinA,cosA,0,0)

以(dx,dy) 為基準點旋轉角度A:transform(cosA, sinA, -sinA, cosA, dx(1-cosA) + dysinA, dy(1-cosA) - dxsinA)

以(dx,dy) 為基準點進行(sx,sy)比例縮放:transform(sx, 0, 0, sy, dx(1-sx), dy(1-sy))

還有很多其他更複雜的變形,大家可以參考圖形學相關資料。

下面給出一個基準點變形的例子,滑鼠在影像上的某點保持按下狀態,影像就會以該點為基準點進行縮放或旋轉,鬆開按鈕後影像復原。

提示:即縮放又旋轉這個例子,並沒有使用變形矩陣,而是用了四步驟簡單變形複合而成。效果如下:


移動-在影像上按住滑鼠並移動
基準點縮放-在影像某處按住滑鼠
基準點旋轉-在影像某點處按住滑鼠
基準點縮放同時旋轉-在影像某處按住滑鼠

 

四、組合

#所謂組合就是一個圖形繪製在另一個圖形之上,會出現什麼效果。預設的情況下是上面的圖形覆蓋了下面的圖形,稱之為 source-over 。

上下文物件總共十二中組合類型,屬性globalCompositeOperation 被用來設定組合類型,如下:globalCompositeOperation = type#type 是下面12 種字串值之一:

注意

:上面所有例子中,

藍色

方塊是

先繪製

的,即“已有的canvas 內容”, 紅色圓形是

後面繪製

,即「新圖形」。

五、裁切路徑

在第一篇文章中我們就介紹了上下文物件的兩大類繪製方法,也就是繪製線條的

stroke

系列方法和填滿區域的

fill

系列方法,其實,上下文物件還有一類別繪製方法叫做裁剪

clip

什麼是裁切呢?打個不恰當的比方吧,你用一塊布把電視機螢幕遮住了,這時候電視機螢幕上的任何變化你都看不見了。

但是,如果你布上裁剪出一塊區域,那麼至少這塊區域裡的螢幕變化你能看見。

當然

裁剪區域之外的螢幕也在不停地變化(即也在重新繪製),只是你看不見罷了

。這就是所謂的裁剪,平常在處理影像時常會遇到這種需求。

那麼什麼又是

裁切路徑

呢?上面說要在布上裁剪出一塊區域,這塊區域是怎麼來的呢?

這塊區域是在裁剪動作

clip

之前,由繪圖路徑設定的,他可以是方形、圓形、五星形和其他任何可以繪製的輪廓形狀。

所以,裁切路徑其實就是繪圖路徑,只不過這個路徑不是拿來繪圖的,而是設定顯示區域和遮蔽區域的一個分界線。

如果你不明白什麼是繪圖路徑,在前的文章 HTML5邊玩邊學(2):基礎繪圖 中有介紹。

下面的範例用了兩種方法來裁切。第一種方法顯示一來回移動的圓形裁切區域,大體流程如下:

###1、清空畫布######2、改變圓心位置######3、在新的圓心位置處設置一個圓形的裁剪區域######4、在畫布上繪製美女圖像#######由於我們不停地在新位置處設定裁剪區域,我們就能看見裁剪區域在移動,而裁剪區域之外的圖像並沒有顯示出來######我們用clip 方法設定裁剪區域,之後繪製的圖形只能顯示裁剪區域內的一部分,而裁剪區域之外總是顯示畫布的背景色。 ######假如並不想完全遮擋裁剪區域之外的圖像,例如我們想讓裁剪區域之內的圖像完全顯示出來,但是裁剪區域之外的圖像以半透明的方式顯示出來,該怎麼做呢? ######這就要用到我們上面說的組合知識了。第二中方法顯示半透明的遮擋,大體思路如下:######1、清空畫布######2、將畫布的所有區域用一種半透明的顏色填充,這裡我用的是灰色,透明度0.9######3、改變圓心位置###

4、在新的圆心位置处以 XOR 方式绘制圆,这样和圆形重叠的部分将被擦除掉
这时候我们得到的图形效果是一个半透明的画布,上面有一块完全透明的圆形区域

5、 在第 4 步的基础上,以 destination-over 方式绘制美女图像,这时候美女图像将会出现在第 4 步图形效果的下方,想象一下,正好是我们想要的效果吧?!

代码



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--><canvas id="canvas1" width="250" height="300" 

    onmousedown="trans.transform(event);"  

    onmouseup="trans.init(event);" 

    onmousemove="trans.translate(event);" 

    style="background-color:black">

    你的浏览器不支持 &lt;canvas&gt;标签,请使用 Chrome 浏览器 或者 FireFox 浏览器

</canvas><br/>

<input type="radio" name="r" id="r1" checked="checked">移动——在图像上按住鼠标并移动<br />

<input type="radio" name="r" id="r2">基准点缩放——在图像某点处按住鼠标<br />

<input type="radio" name="r"  id="r3">基准点旋转——在图像某点处按住鼠标<br />

<input type="radio" name="r"  id="r4">基准点缩放同时旋转——在图像某点处按住鼠标<br />



<canvas id="canvas3" width="250" height="300" style="background-color:black">

    你的浏览器不支持 &lt;canvas&gt;标签,请使用 Chrome 浏览器 或者 FireFox 浏览器

</canvas><br/>

<input type="button" onclick="move(1);" value="移动裁剪区域">

<input type="button" onclick="move(2);" value="移动蒙版">

<input type="button" onclick="stop();" value="停止移动"><br />



        <div>

            <table>

                <tr>

                    <td><canvas id="tut0" width="125" height="125"></canvas><br/><label id="lab0"></label></td>

                <td><canvas id="tut1" width="125" height="125"></canvas><br/><label id="lab1"></label></td>

                <td><canvas id="tut2" width="125" height="125"></canvas><br/><label id="lab2"></label></td>

                <td><canvas id="tut3" width="125" height="125"></canvas><br/><label id="lab3"></label></td>

                </tr>

                <tr>

                    <td><canvas id="tut4" width="125" height="125"></canvas><br/><label id="lab4"></label></td>

                <td><canvas id="tut5" width="125" height="125"></canvas><br/><label id="lab5"></label></td>

                <td><canvas id="tut6" width="125" height="125"></canvas><br/><label id="lab6"></label></td>

                <td><canvas id="tut7" width="125" height="125"></canvas><br/><label id="lab7"></label></td>

                </tr>

                <tr>

                    <td><canvas id="tut8" width="125" height="125"></canvas><br/><label id="lab8"></label></td>

                <td><canvas id="tut9" width="125" height="125"></canvas><br/><label id="lab9"></label></td>

                <td><canvas id="tut10" width="125" height="125"></canvas><br/><label id="lab10"></label></td>

                <td><canvas id="tut11" width="125" height="125"></canvas><br/><label id="lab11"></label></td>

                </tr>

            </table>

        </div>





<script type="text/javascript">

    //美女图的 Base64 编码

    IMG_SRC=&#39;data:image/gif;base64,/9j/4QDfRXhpZgAASUkqAAgAAAAFABIBAwA......&#39;;//省略四十字节



    //==========================================

    //基准点变形类

    //==========================================

    function Transform(){

        //获取画布对象

        this.ctx = document.getElementById("canvas1").getContext("2d");

        //创建图像对象

        this.img=new Image();

        //指定图像源

        this.img.src=IMG_SRC;

        this.interval = null;

        //鼠标按钮状态

        this.pressed=false;

        this.init();

    }

    

    //初始化图形

    Transform.prototype.init=function(){

        //鼠标按钮状态

        this.pressed=false;

        //停止计时器

        if(this.interval) clearInterval(this.interval);

        //变化值

        this.delta = 0.06;

        //清空

        this.ctx.clearRect(0,0,250,300);

        //重绘

        this.paint();

    }

    

    //绘制图像

    Transform.prototype.paint=function(){

        var that=this;

        var img=this.img

        if(img.complete)

            that.ctx.drawImage(img,0,0);

        else 

            var interval = setInterval(function(){

                if(img.complete){

                    that.ctx.drawImage(img,0,0);

                    clearInterval(interval);

                }

            },300);

    }

    

    //鼠标按钮按下后,开始变形

    Transform.prototype.transform = function(){

        //获取基准点

        this.dx=event.offsetX;

        this.dy=event.offsetY;

        //获取基准点

        this.startx=event.offsetX;

        this.starty=event.offsetY;

        //初始缩放比例

        this.sc=1;

        //初旋转角度

        this.angle=0;

        

        var that=this;

        if(document.getElementById("r1").checked)

            //鼠标按钮状态

            this.pressed=true;

        else if(document.getElementById("r2").checked)

            this.interval = setInterval(function(){that.scale()},50);

        else if((document.getElementById("r3").checked))

            this.interval = setInterval(function(){that.rotate()},50);

        else 

            this.interval = setInterval(function(){that.scaleAndRotate()},50);

    }

    

    //移动

    Transform.prototype.translate = function(){

        this.ddx=event.offsetX-this.startx;

        this.ddy=event.offsetY-this.starty;

        if(this.pressed){

            //清空

            this.ctx.clearRect(0,0,250,300);

            //保存状态

            this.ctx.save();

            //平移

            this.ctx.translate(this.ddx,this.ddy);

            //重绘

            this.paint();

            //绘制基准点

            this.ctx.fillStyle="red";

            this.ctx.fillRect(this.dx-5,this.dy-5,10,10);

            //恢复状态

            this.ctx.restore();

        }

    }



    //缩放变形

    Transform.prototype.scale = function(){

        //清空

        this.ctx.clearRect(0,0,250,300);

        //改变缩放比例

        this.sc=this.sc - this.delta;

        if(this.sc<0.2 || this.sc>2) 

            this.delta = -this.delta;

        //保存状态

        this.ctx.save();

        //以 (dx,dy) 为基准点进行 (sx,sy)比例缩放:transform(sx, 0, 0, sy, dx(1-sx), dy(1-sy))

        this.ctx.transform(this.sc, 0, 0, this.sc, this.dx*(1-this.sc), this.dy*(1-this.sc))

        //用新的变形矩阵重绘

        this.paint();

        //绘制基准点

        this.ctx.fillStyle="red";

        this.ctx.fillRect(this.dx-5,this.dy-5,10,10);

        //恢复状态

        this.ctx.restore();

    }

    

    //旋转变形

    Transform.prototype.rotate = function(){

        //清空

        this.ctx.clearRect(0,0,250,300);

        //改变缩放比例

        var PI = Math.PI;

        this.angle=this.angle + PI/60;

        //保存状态

        this.ctx.save();

        //以 (dx,dy) 为基准点旋转角度 A:transform(cosA, sinA, -sinA, cosA, dx(1-cosA) + dysinA, dy(1-cosA) - dxsinA)

        this.ctx.transform(Math.cos(this.angle), Math.sin(this.angle), 

                -Math.sin(this.angle), Math.cos(this.angle), 

                this.dx*(1-Math.cos(this.angle)) + this.dy*Math.sin(this.angle), 

                this.dy*(1-Math.cos(this.angle)) - this.dx*Math.sin(this.angle))

        //用新的变形矩阵重绘

        this.paint();

        //绘制基准点

        this.ctx.fillStyle="red";

        this.ctx.fillRect(this.dx-5,this.dy-5,10,10);

        //恢复状态

        this.ctx.restore();

    }

    

    //即缩放又旋转变形,没有使用变形矩阵

    Transform.prototype.scaleAndRotate = function(){

        //清空

        this.ctx.clearRect(0,0,250,300);

        //改变缩放比例

        this.sc=this.sc - this.delta;

        if(this.sc<0.2 || this.sc>2) 

            this.delta = -this.delta;

        var PI = Math.PI;

        this.angle=this.angle + PI/60;

        //保存状态

        this.ctx.save();

        //先移动原点到基点

        this.ctx.translate(this.dx,this.dy);

        this.ctx.scale(this.sc,this.sc);

        this.ctx.rotate(this.angle);

        this.ctx.translate(-this.dx,-this.dy);

        //用新的变形矩阵重绘

        this.paint();

        //绘制基准点

        this.ctx.fillStyle="red";

        this.ctx.fillRect(this.dx-5,this.dy-5,10,10);

        //恢复状态

        this.ctx.restore();

    }

    

    var trans = new Transform();

    

    //==========================================

    function Clip(){

        var canvas = document.getElementById("canvas3");

        this.ctx = canvas.getContext("2d");

        this.img=new Image();

        this.img.src=IMG_SRC;

        //移动方向

        this.delta=[3,3];

        //起始点

        this.pos_x = 225;

        this.pos_y = 120;

        //半径

        this.radius = 40;

        //画布的长和宽

        this.w = parseInt(canvas.getAttribute("width"));

        this.h = parseInt(canvas.getAttribute("height"));

    }

    

    Clip.prototype.draw1=function(){

        //碰撞检测

        if (this.pos_x < this.radius) {

            this.delta[0] = Math.random() % 4 + 5;

        } else if (this.pos_x > this.w - this.radius) {

            this.delta[0] = -(Math.random() % 4 + 5);

        }

        if (this.pos_y < this.radius) {

            this.delta[1] = Math.random() % 4 + 5;

        } else if (this.pos_y > this.h - this.radius) {

            this.delta[1] = -(Math.random() % 4 + 5);

        }

        this.pos_x += this.delta[0];

        this.pos_y += this.delta[1];



        this.ctx.clearRect(0, 0, this.w, this.h);

        //保存状态

        this.ctx.save()

        //移动变形

        this.ctx.translate(this.pos_x,this.pos_y);

        //设置裁剪区域

        this.ctx.beginPath();

        this.ctx.arc(0 ,0,this.radius,0,Math.PI*2,true);

        this.ctx.clip();         

        // 将图片画到画布上

        this.ctx.drawImage(this.img, -this.pos_x, -this.pos_y,this.w, this.h);

        //恢复状态

        this.ctx.restore();

    }

    

    Clip.prototype.draw2=function(){

        //碰撞检测

        if (this.pos_x < this.radius) {

            this.delta[0] = Math.random() % 4 + 5;

        } else if (this.pos_x > this.w - this.radius) {

            this.delta[0] = -(Math.random() % 4 + 5);

        }

        if (this.pos_y < this.radius) {

            this.delta[1] = Math.random() % 4 + 5;

        } else if (this.pos_y > this.h - this.radius) {

            this.delta[1] = -(Math.random() % 4 + 5);

        }

        this.pos_x += this.delta[0];

        this.pos_y += this.delta[1];



        this.ctx.clearRect(0, 0, this.w, this.h);

        //绘制灰色的半透明蒙版

        this.ctx.fillStyle="rgba(125,125,125,0.9)"

        this.ctx.fillRect(0, 0, this.w, this.h);

        //保存状态

        this.ctx.save()

        //移动坐标

        this.ctx.translate(this.pos_x,this.pos_y);

        //裁剪透明的圆形区域

        this.ctx.globalCompositeOperation = "xor";   

        this.ctx.fillStyle="white"

        this.ctx.beginPath();

        this.ctx.arc(0 ,0,this.radius,0,Math.PI*2,true);

        this.ctx.fill();       

        // 将图片画到蒙版的下面,即只露出透明区域

        this.ctx.globalCompositeOperation = "destination-over";   

        this.ctx.drawImage(this.img, -this.pos_x, -this.pos_y,this.w, this.h);

        //恢复状态

        this.ctx.restore();

    }



    var cl=new Clip();

    cl.interval=null;

    

    function move(id){      

        if(cl.interval)

            clearInterval(cl.interval)

        if(id==1){

            cl.ctx.clearRect(0, 0, 450, 300);    

            cl.interval=setInterval(function(){cl.draw1()},20);

        }

        else{

            cl.ctx.clearRect(0, 0, 450, 300);    

            cl.interval=setInterval(function(){cl.draw2()},20);

        }

    }



    function stop(){

        clearInterval(cl.interval)

    }

    

    var compositeTypes = [

        &#39;source-over&#39;,&#39;source-in&#39;,&#39;source-out&#39;,&#39;source-atop&#39;,

        &#39;destination-over&#39;,&#39;destination-in&#39;,&#39;destination-out&#39;,&#39;destination-atop&#39;,

        &#39;lighter&#39;,&#39;darker&#39;,&#39;copy&#39;,&#39;xor&#39;

    ];

    function drawComp(){

        for (i=0;i<compositeTypes.length;i++){

            var label = document.createTextNode(compositeTypes[i]);

            document.getElementById(&#39;lab&#39;+i).appendChild(label);

            var ctx = document.getElementById(&#39;tut&#39;+i).getContext(&#39;2d&#39;);



            // draw rectangle

            ctx.fillStyle = "#09f";

            ctx.fillRect(15,15,70,70);



            // set composite property

            ctx.globalCompositeOperation = compositeTypes[i];



            // draw circle

            ctx.fillStyle = "#f30";

            ctx.beginPath();

            ctx.arc(75,75,35,0,Math.PI*2,true);

            ctx.fill();

        }

    }

    drawComp();

</script>

以上是HTML5邊玩邊學(六)-汽車人,變形的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn