十四、使用 Canvas 繪圖

??HTML5 添加的最受歡迎的功能就是<canvas>元素。這個(gè)元素負(fù)責(zé)在頁面中設(shè)定一個(gè)區(qū)域,然后就可以通過 JavaScript 動(dòng)態(tài)地在這個(gè)區(qū)域中繪制圖形。

??<canvas>元素最早是由蘋果公司推出的,當(dāng)時(shí)主要用在其 Dashboard 微件中。

??很快,HTML5 加入了這個(gè)元素,主流瀏覽器也迅速開始支持它。IE9+、Firefox 1.5+、Safari 2+、Opera 9+、Chrome、iOS 版 Safari 以及 Android 版 WebKit 都在某種程度上支持<canvas>。

??與瀏覽器環(huán)境中的其他組件類似,<canvas>由幾組 API 構(gòu)成,但并非所有瀏覽器都支持所有這些 API。除了具備基本繪圖能力的 2D 上下文,<canvas>還建議了一個(gè)名為 WebGL 的 3D 上下文。

??目前,支持該元素的瀏覽器都支持 2D 上下文及文本 API,但對 WebGL 的支持還不夠好。由于 WebGL 還是實(shí)驗(yàn)性的,因此要得到所有瀏覽器支持還需要很長一段時(shí)間。Firefox 4+和 Chrome 支持 WebGL 規(guī)范的早期版本,但一些老版本的操作系統(tǒng),比如 Windows XP,由于缺少必要的繪圖驅(qū)動(dòng)程序,即便安裝了這兩款瀏覽器也無濟(jì)于事。

1、基本用法

??要使用 <canvas> 元素,必須先設(shè)置 width 和 height 屬性,指定可以繪圖的區(qū)域大小。出現(xiàn)在開始和結(jié)束標(biāo)簽中的內(nèi)容是后備信息,如果瀏覽器不支持 <canvas> 元素,就會(huì)顯示這些信息。下面就是 <canvas> 元素的例子。

<canvas id="drawing" width="200" height="200">A drawing of something.</canvas>

??與其它元素一樣,<canvas> 元素對應(yīng)的 DOM 元素對象也有 width 和 height 屬性,可以隨意修改。而且,也能通過 CSS 為該元素添加樣式,如果不添加任何樣式或者不繪制任何圖形,在頁面中是看不到該元素的。
??要在這塊畫布(canvas)上繪圖,需要取得繪圖上下文。而取得繪圖上下文對象的引用,需要調(diào)用 getContext() 方法并傳入上下文的名字。傳入"2d",就可以取得 2D 上下文對象。

var drawing = document.getElementById('drawing');

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext) {
    var context = drawing.getContext("2d");
    // 更多代碼
}

??在使用 <canvas> 元素之前,首先要檢測 getContext() 方法是否存在,這一步非常重要。有些瀏覽器會(huì)為 HTML 規(guī)范之外的元素創(chuàng)建默認(rèn)的 HTML 元素對象(假設(shè)你想在 Firefox3 中使用 <canvas> 元素,雖然瀏覽器會(huì)為該標(biāo)簽創(chuàng)建一個(gè) DOM 對象,但這個(gè)對象中并沒有 getContext() 方法。)。在這種情況下,即使 drawing 變量中保存著一個(gè)有效的元素引用,也檢測不到 getContext() 方法。
??使用 toDataURL() 方法,可以導(dǎo)出在<canvas>元素上繪制的圖像。這個(gè)方法接受一個(gè)參數(shù),即圖像的 MIME 類型格式,而且適合用于創(chuàng)建圖像的任何上下文。比如,要取得畫布中的一幅 PNG 格式的圖像,可以使用以下代碼。

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext()) {
    // 取得圖像的數(shù)據(jù) URI
    var imgURI = drawing.toDataURL("image/png");

    // 顯示圖像
    var image = document.creatElement("img");
    image.src = imgURI;
    document.body.appendChild(image);
}

??默認(rèn)情況下,瀏覽器會(huì)將圖像編碼為 PNG 格式(除非另行指定)。Firefox 和 Opera 也支持基于 "image/jpeg" 參數(shù)的 JPEG 編碼格式。由于這個(gè)方法是后來才追加的,所以支持<canvas>的瀏覽器也是在較新的版本中才加入了對它的支持,比如 IE9、Firefox3.5 和 Opera10。

如果繪制到畫布上的圖像源自不同的域,toDataURL() 方法會(huì)拋出錯(cuò)誤。

2、2D 上下文

??使用 2D 繪圖上下文提供的方法,可以繪制簡單的 2D 圖形,比如矩形、弧線和路徑。
??2D 上下文的坐標(biāo)開始于<canvas>元素的左上角,原點(diǎn)坐標(biāo)是(0,0)。所有坐標(biāo)值都基于這個(gè)原點(diǎn)計(jì)算,x 值越大表示越靠右,y 值越大表示越靠下。默認(rèn)情況下,width 和 height 表示水平和垂直兩個(gè)方向上可用的像素?cái)?shù)目。

2.1、 填充和描邊

??2D 上下文的兩種基本繪圖操作是填充和描邊。

??填充,就是用指定的樣式(顏色、漸變或圖像)填充圖形;

??描邊,就是只在圖形的邊緣畫線。

??大多數(shù) 2D 上下文操作都會(huì)細(xì)分為填充和描邊兩個(gè)操作,而操作的結(jié)果取決于兩個(gè)屬性:fillStyle 和 strokeStyle。這兩個(gè)屬性的值可以是字符串、漸變對象或模式對象,而且它們的默認(rèn)值是"#000000"。
??如果為它們指定表示顏色的字符串值,可以使用 CSS 中指定顏色值的任何格式,包括顏色名、十六進(jìn)制碼、rgb、rgba、hsl 或 hsla。示例:

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
    var context = drawing.getContext("2d");
    context.strokeStyle = "red";
    context.fillStyle = "#0000ff";
}

??以上代碼將 strokeStyle 設(shè)置為 red(CSS 中的顏色名),將 fillStyle 設(shè)置為#0000ff(藍(lán)色)。
??然后,所有涉及描邊和填充的操作都將使用這兩個(gè)樣式,直至重新設(shè)置這兩個(gè)值。如前所述,這兩個(gè)屬性的值也可以是漸變對象或模式對象。本章后面會(huì)討論這兩種對象。

2.2、 繪制矩形

??矩形是唯一一種可以直接在 2D 上下文中繪制的形狀。與矩形有關(guān)的方法包括 fillRect()、strokeRect() 和 clearRect()。
??這三個(gè)方法都能接收 4 個(gè)參數(shù):矩形的 x 坐標(biāo)、矩形的 y 坐標(biāo)、矩形寬度和矩形高度。這些參數(shù)的單位都是像素。

??首先,fillRect() 方法在畫布上繪制的矩形會(huì)填充指定的顏色。填充的顏色通過 fillStyle 屬性指定,比如:

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
    var context = drawing.getContext("2d");

    // 繪制紅色矩形
    context.fillStyle = "#ff0000";
    context.fillRect(10, 10, 50, 50);

    // 繪制半透明的藍(lán)色矩形
    context.fillStyle = "rgba(0, 0, 255, .5)";
    context.fillRect(30, 30, 50, 50);
}

??以上代碼首先將 fillStyle 設(shè)置為紅色,然后從(10, 10)處開始繪制矩形,矩形的寬和高均為 50 像素。然后,通過 rgba() 格式再將 fillStyle 設(shè)置為半透明的藍(lán)色,在第一個(gè)矩形上面繪制第二個(gè)矩形。結(jié)果就是可以透過藍(lán)色的矩形看到紅色的矩形。

??strokeRect() 方法在畫布上繪制的矩形會(huì)使用指定的顏色描邊。描邊顏色通過 strokeStyle 屬性指定。示例:

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
    var context = drawing.getContext("2d");

    // 繪制紅色描邊矩形
    context.strokeStyle= "#ff0000";
    context.strokeRect(10, 10, 50, 50);

    // 繪制半透明的藍(lán)色描邊矩形
    context.strokeStyle= "rgba(0, 0, 255, .5)";
    context.strokeRect(30, 30, 50, 50);
}

??以上代碼繪制了兩個(gè)重疊的矩形。不過,這兩個(gè)矩形都只有框線,內(nèi)部并沒有填充顏色。

??描邊線條的寬度由 lineWidth 屬性控制,該屬性的值可以是任意整數(shù)。
??另外,通過 lineCap 屬性可以控制線條末端的形狀是平頭、圓頭還是方頭("butt"、"round" 或 "square")。
??通過 lineJoin 屬性可以控制線條相交的方式是圓交、斜
交還是斜接("round"、"bevel" 或 "miter")。

??最后,clearRect() 方法用于清除畫布上的矩形區(qū)域。本質(zhì)上,這個(gè)方法可以把繪制上下文中的某一矩形區(qū)域變透明。通過繪制形狀然后再清除指定區(qū)域,就可以生成有意思的效果,例如把某個(gè)形狀切掉一塊。示例:

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
    var context = drawing.getContext("2d");

    // 繪制紅色矩形
    context.fillStyle = "#ff0000";
    context.fillRect(10, 10, 50, 50);

    // 繪制半透明的藍(lán)色矩形
    context.fillStyle = "rgba(0, 0, 255, .5)";
    context.fillRect(30, 30, 50, 50);

   // 在兩個(gè)矩形重疊的地方清除一個(gè)小矩形
    context.clearRect(40, 40, 10, 10); 
}

2.3、繪制路徑

??2D 繪制上下文支持很多在畫布上繪制路徑的方法。通過路徑可以創(chuàng)造出復(fù)雜的形狀和線條。

??要繪制路徑,首先必須調(diào)用 beginPath() 方法,表示要開始繪制新路徑。然后,再通過調(diào)用下列方法來實(shí)際地繪制路徑。

  • arc(x, y, radius, startAngle, endAngle, counterclockwise):以(x, y)為圓心繪制一條弧線,弧線半徑為 radius,起始和結(jié)束角度(用弧度表示)分別為 startAngle 和 endAngle。最后一個(gè)參數(shù)表示 startAngle 和 endAngle 是否按逆時(shí)針方向計(jì)算,值為 false 表示按順時(shí)針方向計(jì)算。
  • arcTo(x1, y1, x2, y2, radius):從上一點(diǎn)開始繪制一條弧線,到(x2, y2)為止,并且以給定的半徑 radius 穿過(x1, y1)。
  • bezierCurveTo(c1x, c1y, c2x, c2y, x, y):從上一點(diǎn)開始繪制一條曲線,到(x, y) 為止,并且以 (c1x, c1y) 和 (c2x, c2y) 為控制點(diǎn)。
  • lineTo(x, y):從上一點(diǎn)開始繪制一條直線,到(x, y)為止。
  • moveTo(x, y):將繪圖游標(biāo)移動(dòng)到(x, y),不畫線。
  • quadraticCurveTo(cx, cy, x, y):從上一點(diǎn)開始繪制一條二次曲線,到(x, y)為止,并且以(cx, cy)作為控制點(diǎn)。
  • rect(x, y, width, height):從點(diǎn)(x, y)開始繪制一個(gè)矩形,寬度和高度分別由 width 和 height 指定。這個(gè)方法繪制的是矩形路徑,而不是 strokeRect() 和 fillRect() 所繪制的獨(dú)立的形狀。

??創(chuàng)建了路徑后,接下來有幾種可能的選擇。如果想繪制一條連接到路徑起點(diǎn)的線條,可以調(diào)用 closePath()。

??如果路徑已經(jīng)完成,你想用 fillStyle 填充它,可以調(diào)用 fill() 方法。

??另外,還可以調(diào)用 stroke() 方法對路徑描邊,描邊使用的是 strokeStyle。

??最后還可以調(diào)用 clip(),這個(gè)方法可以在路徑上創(chuàng)建一個(gè)剪切區(qū)域。

??下面看一個(gè)例子,即繪制一個(gè)不帶數(shù)字的時(shí)鐘表盤。

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){

    var context = drawing.getContext("2d");

    // 開始路徑
    context.beginPath();

    // 繪制外圓
    context.arc(100, 100, 99, 0, 2 * Math.PI, false);

    // 繪制內(nèi)圓
    context.moveTo(194, 100);
    context.arc(100, 100, 94, 0, 2 * Math.PI, false);

    // 繪制分針
    context.moveTo(100, 100);
    context.lineTo(100, 15);

    // 繪制時(shí)針
    context.moveTo(100, 100);
    context.lineTo(35, 100);

    // 描邊路徑
    context.stroke();
}

??上述例子使用 arc() 方法繪制了兩個(gè)圓形:一個(gè)外圓和一個(gè)內(nèi)圓,構(gòu)成了表盤的邊框。外圓的半徑是 99 像素,圓心位于點(diǎn)(100,100),也是畫布的中心點(diǎn)。為了繪制一個(gè)完整的圓形,我們從 0 弧度開始,繪制 2π 弧度(通過 Math.PI 來計(jì)算)。
??在繪制內(nèi)圓之前,必須把路徑移動(dòng)到內(nèi)圓上的某一點(diǎn),以避免繪制出多余的線條。
??第二次調(diào)用 arc() 使用了小一點(diǎn)的半徑,以便創(chuàng)造邊框的效果。
??然后,組合使用 moveTo() 和 lineTo() 方法來繪制時(shí)針和分針。
??最后一步是調(diào)用 stroke() 方法,這樣才能把圖形繪制到畫布上,如下圖所示:

??在 2D 繪圖上下文中,路徑是一種主要的繪圖方式,因?yàn)槁窂侥転橐L制的圖形提供更多控制。
??由于路徑的使用很頻繁,所以就有了一個(gè)名為 isPointInPath() 的方法。這個(gè)方法接收 x 和 y 坐標(biāo)作為參數(shù),用于在路徑被關(guān)閉之前確定畫布上的某一點(diǎn)是否位于路徑上,例如:

if (context.isPointInPath(100, 100)){
    alert("Point (100, 100) is in the path.");
}

??2D 上下文中的路徑 API 已經(jīng)非常穩(wěn)定,可以利用它們結(jié)合不同的填充和描邊樣式,繪制出非常復(fù)雜的圖形來。

2.4、 繪制文本

??文本與圖形總是如影隨形。為此,2D 繪圖上下文也提供了繪制文本的方法。

??繪制文本主要有兩個(gè)方法:fillText() 和 strokeText()。這兩個(gè)方法都可以接收 4 個(gè)參數(shù):要繪制的文本字符串、x 坐標(biāo)、y 坐標(biāo)和可選的最大像素寬度。而且,這兩個(gè)方法都以下列 3 個(gè)屬性為基礎(chǔ)。

  • font:表示文本樣式、大小及字體,用 CSS 中指定字體的格式來指定,例如"10px Arial"。
  • textAlign:表示文本對齊方式。可能的值有 "start"、"end"、"left"、"right" 和 "center"。建議使用"start"和"end",不要使用"left"和"right",因?yàn)榍皟烧叩囊馑几€(wěn)妥,能同時(shí)適合從左到右和從右到左顯示(閱讀)的語言。
  • textBaseline:表示文本的基線。可能的值有 "top"、"hanging"、"middle"、 "alphabetic"、"ideographic" 和 "bottom"。

??這幾個(gè)屬性都有默認(rèn)值,因此沒有必要每次使用它們都重新設(shè)置一遍值。

??fillText() 方法使用 fillStyle 屬性繪制文本,而 strokeText() 方法使用 strokeStyle 屬性為文本描邊。
??相對來說,還是使用 fillText() 的時(shí)候更多,因?yàn)樵摲椒7铝嗽诰W(wǎng)頁中正常顯示文本。例如,下面的代碼在前一節(jié)創(chuàng)建的表盤上方繪制了數(shù)字 12:

context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20); 

??因?yàn)檫@里把 textAlign 設(shè)置為"center",把 textBaseline 設(shè)置為"middle",所以坐標(biāo)(100, 20) 表示的是文本水平和垂直中點(diǎn)的坐標(biāo)。
??如果將 textAlign 設(shè)置為"start",則 x 坐標(biāo)表示的是文本左端的位置(從左到右閱讀的語言);設(shè)置為"end",則 x 坐標(biāo)表示的是文本右端的位置(從左到右閱讀的
語言)。例如:

// 正常
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);

// 起點(diǎn)對齊
context.textAlign = "start";
context.fillText("12", 100, 40);

// 終點(diǎn)對齊
context.textAlign = "end";
context.fillText("12", 100, 60);

??這一回繪制了三個(gè)字符串"12",每個(gè)字符串的 x 坐標(biāo)值相同,但 textAlign 值不同。另外,后兩個(gè)字符串的 y 坐標(biāo)依次增大,以避免相互重疊。結(jié)果如下圖所示:

??表盤中的分針恰好位于正中間,因此文本的水平對齊方式如何變化也能夠一目了然。

??類似地,修改 textBaseline 屬性的值可以調(diào)整文本的垂直對齊方式:值為"top",y 坐標(biāo)表示文本頂端;值為 "bottom",y 坐標(biāo)表示文本底端;值為 "hanging"、"alphabetic" 和 "ideographic",則 y 坐標(biāo)分別指向字體的特定基線坐標(biāo)。

??由于繪制文本比較復(fù)雜,特別是需要把文本控制在某一區(qū)域中的時(shí)候,2D 上下文提供了輔助確定文本大小的方法 measureText()。
??這個(gè)方法接收一個(gè)參數(shù),即要繪制的文本;返回一個(gè) TextMetrics 對象。返回的對象目前只有一個(gè) width 屬性,但將來還會(huì)增加更多度量屬性。

??measureText() 方法利用 font、textAlign 和 textBaseline 的當(dāng)前值計(jì)算指定文本的大小。
??比如,假設(shè)你想在一個(gè) 140 像素寬的矩形區(qū)域中繪制文本 Hello world!,下面的代碼從 100 像素的字體大小開始遞減,最終會(huì)找到合適的字體大小。

var fontSize = 100;
context.font = fontSize + "px Arial";
while(context.measureText("Hello world!").width > 140){
    fontSize--;
    context.font = fontSize + "px Arial";
}
context.fillText("Hello world!", 10, 10);
context.fillText("Font size is " + fontSize + "px", 10, 50); 

??前面提到過,fillText 和 strokeText() 方法都可以接收第四個(gè)參數(shù),也就是文本的最大像素寬度。不過,這個(gè)可選的參數(shù)尚未得到所有瀏覽器支持(最早支持它的是 Firefox 4)。
??提供這個(gè)參數(shù)后,調(diào)用 fillText() 或 strokeText() 時(shí)如果傳入的字符串大于最大寬度,則繪制的文本字符的高度正確,但寬度會(huì)收縮以適應(yīng)最大寬度。
??繪制文本還是相對比較復(fù)雜的操作,因此支持<canvas>元素的瀏覽器也并未完全實(shí)現(xiàn)所有與繪制文本相關(guān)的 API。

2.5、變換

??通過上下文的變換,可以把處理后的圖像繪制到畫布上。2D 繪制上下文支持各種基本的繪制變換。
??創(chuàng)建繪制上下文時(shí),會(huì)以默認(rèn)值初始化變換矩陣,在默認(rèn)的變換矩陣下,所有處理都按描述直接繪制。
??為繪制上下文應(yīng)用變換,會(huì)導(dǎo)致使用不同的變換矩陣應(yīng)用處理,從而產(chǎn)生不同的結(jié)果。
??可以通過如下方法來修改變換矩陣。

  • rotate(angle):圍繞原點(diǎn)旋轉(zhuǎn)圖像 angle 弧度。
  • scale(scaleX, scaleY):縮放圖像,在 x 方向乘以 scaleX,在 y 方向乘以 scaleY。scaleX 和 scaleY 的默認(rèn)值都是 1.0。
  • translate(x, y):將坐標(biāo)原點(diǎn)移動(dòng)到(x,y)。執(zhí)行這個(gè)變換之后,坐標(biāo)(0,0)會(huì)變成之前由(x, y)表示的點(diǎn)。
  • transform(m1_1, m1_2, m2_1, m2_2, dx, dy):直接修改變換矩陣,方式是乘以如下矩陣。
    m1_1??m1_2??dx
    m2_1??m2_2??dy
    0????0????1
  • setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):將變換矩陣重置為默認(rèn)狀態(tài),然后再調(diào)用 transform()。

??變換有可能很簡單,但也可能很復(fù)雜,這都要視情況而定。比如,就拿前面例子中繪制表針來說,如果把原點(diǎn)變換到表盤的中心,然后再繪制表針就容易多了。請看下面的例子。

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){ 

    var context = drawing.getContext("2d");

    // 開始路徑
    context.beginPath();

    // 繪制外圓
    context.arc(100, 100, 99, 0, 2 * Math.PI, false);

    // 繪制內(nèi)圓
    context.moveTo(194, 100);
    context.arc(100, 100, 94, 0, 2 * Math.PI, false);

    // 變換原點(diǎn)
    context.translate(100, 100);

    // 繪制分針
    context.moveTo(0,0);
    context.lineTo(0, -85);

    // 繪制時(shí)針
    context.moveTo(0, 0);
    context.lineTo(-65, 0);

    // 描邊路徑
    context.stroke();
}

??把原點(diǎn)變換到時(shí)鐘表盤的中心點(diǎn)(100, 100)后,在同一方向上繪制線條就變成了簡單的數(shù)學(xué)問題了。所有數(shù)學(xué)計(jì)算都基于(0, 0),而不是(100, 100)。
??還可以更進(jìn)一步,像下面這樣使用 rotate() 方法旋轉(zhuǎn)時(shí)鐘的表針。

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){ 

    var context = drawing.getContext("2d");

    // 開始路徑
    context.beginPath();

    // 繪制外圓
    context.arc(100, 100, 99, 0, 2 * Math.PI, false);

    // 繪制內(nèi)圓
    context.moveTo(194, 100);
    context.arc(100, 100, 94, 0, 2 * Math.PI, false);

    // 變換原點(diǎn)
    context.translate(100, 100);

     // 旋轉(zhuǎn)表針
     context.rotate(1);

    // 繪制分針
    context.moveTo(0,0);
    context.lineTo(0, -85);

    // 繪制時(shí)針
    context.moveTo(0, 0);
    context.lineTo(-65, 0);

    // 描邊路徑
    context.stroke();
}

??因?yàn)樵c(diǎn)已經(jīng)變換到了時(shí)鐘表盤的中心點(diǎn),所以旋轉(zhuǎn)也是以該點(diǎn)為圓心的。結(jié)果就像是表針真地被固定在表盤中心一樣,然后向右旋轉(zhuǎn)了一定角度。

??無論是剛才執(zhí)行的變換,還是 fillStyle、strokeStyle 等屬性,都會(huì)在當(dāng)前上下文中一直有效,除非再對上下文進(jìn)行什么修改。
??雖然沒有什么辦法把上下文中的一切都重置回默認(rèn)值,但有兩個(gè)方法可以跟蹤上下文的狀態(tài)變化。如果你知道將來還要返回某組屬性與變換的組合,可以調(diào)用 save() 方法。
??調(diào)用這個(gè)方法后,當(dāng)時(shí)的所有設(shè)置都會(huì)進(jìn)入一個(gè)棧結(jié)構(gòu),得以妥善保管。然后可以對上下文進(jìn)行其他修改。等想要回到之前保存的設(shè)置時(shí),可以調(diào)用 restore() 方法,在保存設(shè)置的棧結(jié)構(gòu)中向前返回一級,
恢復(fù)之前的狀態(tài)。
??連續(xù)調(diào)用 save() 可以把更多設(shè)置保存到棧結(jié)構(gòu)中,之后再連續(xù)調(diào)用 restore() 則可以一級一級返回。下面來看一個(gè)例子。

context.fillStyle = "#ff0000";
context.save();

context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();

context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); // 從點(diǎn)(100,100)開始繪制藍(lán)色矩形

context.restore();
context.fillRect(10, 10, 100, 200); // 從點(diǎn)(110,110)開始繪制綠色矩形

context.restore(); 
context.fillRect(0, 0, 100, 200); // 從點(diǎn)(0,0)開始繪制紅色矩形

??首先,將 fillStyle 設(shè)置為紅色,并調(diào)用 save() 保存上下文狀態(tài)。
??接下來,把 fillStyle 修改為綠色,把坐標(biāo)原點(diǎn)變換到(100, 100),再調(diào)用 save() 保存上下文狀態(tài)。
??然后,把 fillStyle 修改為藍(lán)色并繪制藍(lán)色的矩形。因?yàn)榇藭r(shí)的坐標(biāo)原點(diǎn)已經(jīng)變了,所以矩形的左上角坐標(biāo)實(shí)際上是(100, 100)。
??然后調(diào)用 restore(),之后 fillStyle 變回了綠色,因而第二個(gè)矩形就是綠色。之所以第二個(gè)矩形的起點(diǎn)坐標(biāo)是(110, 110),是因?yàn)樽鴺?biāo)位置的變換仍然起作用。
??再調(diào)用一次 restore(),變換就被取消了,而 fillStyle 也返回了紅色。所以最后一個(gè)矩形是紅色的,而且繪制的起點(diǎn)是(0,0)。
??需要注意的是,save() 方法保存的只是對繪圖上下文的設(shè)置和變換,不會(huì)保存繪圖上下文的內(nèi)容。

2.6、 繪制圖像

?? 繪圖上下文內(nèi)置了對圖像的支持。如果你想把一幅圖像繪制到畫布上,可以使用 drawImage() 方法。根據(jù)期望的最終結(jié)果不同,調(diào)用這個(gè)方法時(shí),可以使用三種不同的參數(shù)組合。最簡單的調(diào)用方式是傳入一個(gè) HTML <img> 元素,以及繪制該圖像的起點(diǎn)的 x 和 y 坐標(biāo)。例如:

var image = document.images[0];
context.drawImage(image, 10, 10);

??這兩行代碼取得了文檔中的第一幅圖像,然后將它繪制到上下文中,起點(diǎn)為(10, 10)。繪制到畫布上的圖像大小與原始大小一樣。
??如果你想改變繪制后圖像的大小,可以再多傳入兩個(gè)參數(shù),分別表示目標(biāo)寬度和目標(biāo)高度。通過這種方式來縮放圖像并不影響上下文的變換矩陣。例如:

context.drawImage(image, 50, 10, 20, 30);

??執(zhí)行代碼后,繪制出來的圖像大小會(huì)變成 20×30 像素。

??除了上述兩種方式,還可以選擇把圖像中的某個(gè)區(qū)域繪制到上下文中。

??drawImage() 方法的這種調(diào)用方式總共需要傳入 9 個(gè)參數(shù):要繪制的圖像、源圖像的 x 坐標(biāo)、源圖像的 y 坐標(biāo)、源圖像的寬度、源圖像的高度、目標(biāo)圖像的 x 坐標(biāo)、目標(biāo)圖像的 y 坐標(biāo)、目標(biāo)圖像的寬度、目標(biāo)圖像的高度。這樣調(diào)用 drawImage() 方法可以獲得最多的控制。例如:

context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60); 

??這行代碼只會(huì)把原始圖像的一部分繪制到畫布上。原始圖像的這一部分的起點(diǎn)為(0, 10),寬和高都是 50 像素。最終繪制到上下文中的圖像的起點(diǎn)是(0, 100),而大小變成了 40×60 像素。
??這種調(diào)用方式可以創(chuàng)造出很有意思的效果,如下圖所示:

??除了給 drawImage() 方法傳入 HTML <img> 元素外,還可以傳入另一個(gè)<canvas>元素作為其第一個(gè)參數(shù)。這樣,就可以把另一個(gè)畫布內(nèi)容繪制到當(dāng)前畫布上。
??結(jié)合使用 drawImage() 和其他方法,可以對圖像進(jìn)行各種基本操作。而操作的結(jié)果可以通過 toDataURL() 方法獲得。(請讀者注意,雖然本章至今一直在討論 2D 繪圖上下文,但 toDataURL()是 Canvas 對象的方法,不是上下文對象的方法。)
??不過,有一個(gè)例外,即圖像不能來自其他域。如果圖像來自其他域,調(diào)用 toDataURL() 會(huì)拋出一個(gè)錯(cuò)誤。打個(gè)比方,假如位于www.example.com 上的頁面繪制的圖像來自于 www.wrox.com,那當(dāng)前上下文就會(huì)被認(rèn)為“不干凈”,因而會(huì)拋出錯(cuò)誤。

2.7、陰影

??2D 上下文會(huì)根據(jù)以下幾個(gè)屬性的值,自動(dòng)為形狀或路徑繪制出陰影。

  • shadowColor:用 CSS 顏色格式表示的陰影顏色,默認(rèn)為黑色。
  • shadowOffsetX:形狀或路徑 x 軸方向的陰影偏移量,默認(rèn)為 0。
  • shadowOffsetY:形狀或路徑 y 軸方向的陰影偏移量,默認(rèn)為 0。
  • shadowBlur:模糊的像素?cái)?shù),默認(rèn) 0,即不模糊。

??這些屬性都可以通過 context 對象來修改。只要在繪制前為它們設(shè)置適當(dāng)?shù)闹担湍茏詣?dòng)產(chǎn)生陰影。例如:

var context = drawing.getContext("2d");

// 設(shè)置陰影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";

// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);

// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0,0,255,1)"; 
context.fillRect(30, 30, 50, 50); 

??兩個(gè)矩形的陰影樣式相同,如下圖所示:

??不同瀏覽器對陰影的支持有一些差異。IE9、Firefox 4 和 Opera 11 的行為最為規(guī)范,其他瀏覽器多多少少會(huì)有一些奇怪的現(xiàn)象,甚至根本不支持陰影。
??Chrome(直至第 10 版)不能正確地為描邊的形狀應(yīng)用實(shí)心陰影。Chrome 和 Safari(直至第 5 版)在為帶透明像素的圖像應(yīng)用陰影時(shí)也會(huì)有問題:不透明部分的下方本來是該有陰影的,但此時(shí)則一概不見了。Safari 也不能給漸變圖形應(yīng)用陰影,其他瀏覽器都可以。

2.8、漸變

??漸變由 CanvasGradient 實(shí)例表示,很容易通過 2D 上下文來創(chuàng)建和修改。要?jiǎng)?chuàng)建一個(gè)新的線性漸變,可以調(diào)用 createLinearGradient() 方法。這個(gè)方法接收 4 個(gè)參數(shù):起點(diǎn)的 x 坐標(biāo)、起點(diǎn)的 y 坐標(biāo)、終點(diǎn)的 x 坐標(biāo)、終點(diǎn)的 y 坐標(biāo)。
??調(diào)用這個(gè)方法后,它就會(huì)創(chuàng)建一個(gè)指定大小的漸變,并返回 CanvasGradient 對象的實(shí)例。
??創(chuàng)建了漸變對象后,下一步就是使用 addColorStop() 方法來指定色標(biāo)。這個(gè)方法接收兩個(gè)參數(shù):色標(biāo)位置和 CSS 顏色值。色標(biāo)位置是一個(gè) 0(開始的顏色)到 1(結(jié)束的顏色)之間的數(shù)字。例如:

var gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");

??此時(shí),gradient 對象表示的是一個(gè)從畫布上點(diǎn)(30, 30)到點(diǎn)(70, 70)的漸變。起點(diǎn)的色標(biāo)是白色,終點(diǎn)的色標(biāo)是黑色。然后就可以把 fillStyle 或 strokeStyle 設(shè)置為這個(gè)對象,從而使用漸變來繪制形狀或描邊:

// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);

// 繪制漸變矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);

??為了讓漸變覆蓋整個(gè)矩形,而不是僅應(yīng)用到矩形的一部分,矩形和漸變對象的坐標(biāo)必須匹配才行。以上代碼會(huì)得到如下圖所示的結(jié)果:

??如果沒有把矩形繪制到恰當(dāng)?shù)奈恢茫强赡芫椭粫?huì)顯示部分漸變效果。例如:

context.fillStyle = gradient; 
context.fillRect(50, 50, 50, 50);

??這兩行代碼執(zhí)行后得到的矩形只有左上角稍微有一點(diǎn)白色。這主要是因?yàn)榫匦蔚钠瘘c(diǎn)位于漸變的中間位置,而此時(shí)漸變差不多已經(jīng)結(jié)束了。由于漸變不重復(fù),所以矩形的大部分區(qū)域都是黑色。如下圖所示:


??確保漸變與形狀對齊非常重要,有時(shí)候可以考慮使用函數(shù)來確保坐標(biāo)合適。例如:

function createRectLinearGradient(context, x, y, width, height){
    return context.createLinearGradient(x, y, x + width, y + height);
}

??這個(gè)函數(shù)基于起點(diǎn)的 x 和 y 坐標(biāo)以及寬度和高度值來創(chuàng)建漸變對象,從而讓我們可以在 fillRect() 中使用相同的值。

var gradient = createRectLinearGradient(context, 30, 30, 50, 50);

gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");

//繪制漸變矩形
context.fi llStyle = gradient;
context.fillRect(30, 30, 50, 50);

??使用畫布的時(shí)候,確保坐標(biāo)匹配很重要,也需要一些技巧。類似 createRectLinearGradient() 這樣的輔助方法可以讓控制坐標(biāo)更容易一些。

??要?jiǎng)?chuàng)建徑向漸變(或放射漸變),可以使用 createRadialGradient() 方法。這個(gè)方法接收 6 個(gè)參數(shù),對應(yīng)著兩個(gè)圓的圓心和半徑。前三個(gè)參數(shù)指定的是起點(diǎn)圓的原心(x 和 y)及半徑,后三個(gè)參數(shù)指定的是終點(diǎn)圓的原心(x 和 y)及半徑。
??可以把徑向漸變想象成一個(gè)長圓桶,而這 6 個(gè)參數(shù)定義的正是這個(gè)桶的兩個(gè)圓形開口的位置。如果把一個(gè)圓形開口定義得比另一個(gè)小一些,那這個(gè)圓桶就變成了圓錐體,而通過移動(dòng)每個(gè)圓形開口的位置,就可達(dá)到像旋轉(zhuǎn)這個(gè)圓錐體一樣的效果。
??如果想從某個(gè)形狀的中心點(diǎn)開始創(chuàng)建一個(gè)向外擴(kuò)散的徑向漸變效果,就要將兩個(gè)圓定義為同心圓。
??比如,就拿前面創(chuàng)建的矩形來說,徑向漸變的兩個(gè)圓的圓心都應(yīng)該在(55, 55),因?yàn)榫匦蔚膮^(qū)域是從(30, 30)到(80,80)。請看代碼:

var gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);

gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");

//繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);

//繪制漸變矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50); 

??運(yùn)行代碼,會(huì)得到如下圖所示的結(jié)果。

??因?yàn)閯?chuàng)建比較麻煩,所以徑向漸變并不那么容易控制。不過,一般來說,讓起點(diǎn)圓和終點(diǎn)圓保持為同心圓的情況比較多,這時(shí)候只要考慮給兩個(gè)圓設(shè)置不同的半徑就好了。

2.9、模式

??模式其實(shí)就是重復(fù)的圖像,可以用來填充或描邊圖形。要?jiǎng)?chuàng)建一個(gè)新模式,可以調(diào)用 createPattern() 方法并傳入兩個(gè)參數(shù):一個(gè) HTML <img> 元素和一個(gè)表示如何重復(fù)圖像的字符串。
??其中,第二個(gè)參數(shù)的值與 CSS 的background-repeat 屬性值相同,包括"repeat"、"repeat-x"、"repeat-y" 和 "no-repeat"。示例:

var image = document.images[0],
    pattern = context.createPattern(image, "repeat");

// 繪制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150); 

??需要注意的是,模式與漸變一樣,都是從畫布的原點(diǎn)(0, 0)開始的。
??將填充樣式(fillStyle)設(shè)置為模式對象,只表示在某個(gè)特定的區(qū)域內(nèi)顯示重復(fù)的圖像,而不是要從某個(gè)位置開始繪制重復(fù)的圖像。
??createPattern() 方法的第一個(gè)參數(shù)也可以是一個(gè)<video>元素,或者另一個(gè)<canvas>元素。

2.10、使用圖像數(shù)據(jù)

??2D 上下文的一個(gè)明顯的長處就是,可以通過 getImageData() 取得原始圖像數(shù)據(jù)。
??這個(gè)方法接收 4 個(gè)參數(shù):要取得其數(shù)據(jù)的畫面區(qū)域的 x 和 y 坐標(biāo)以及該區(qū)域的像素寬度和高度。
??示例,要取得左上角坐標(biāo)為(10, 5)、大小為 50x50 像素的區(qū)域的圖像數(shù)據(jù),可以使用以下代碼:

var imageData = context.getImageData(10, 5, 50, 50);

??上述返回的對象是 ImageData 的實(shí)例。
??每個(gè) ImageData 對象都有三個(gè)屬性:width、height 和 data。
??其中 data 屬性是一個(gè)數(shù)組,保存著圖像中每一個(gè)像素的數(shù)據(jù)。在 data 數(shù)組中,每一個(gè)像素用 4 個(gè)元素來保存,分別表示紅、綠、藍(lán)和透明度。因此,第一個(gè)像素的數(shù)據(jù)就保存在數(shù)組的第 0 到第 3 個(gè)元素中,示例:

var data = imageData,
    red = data[0],
    green = data[1],
    blue = data[2],
    alpha = data[3];

??數(shù)組中的每個(gè)元素的值都介于 0 到 255 之間(包括 0 和 255)。能夠直接訪問到原始圖像數(shù)據(jù),就能夠以各種方式來操作這些數(shù)據(jù)。
??例如,通過修改圖像數(shù)據(jù),可以像下面這樣創(chuàng)建一個(gè)簡單的灰階過濾器。

var drawing = document.getElementById("drawing");

// 確定瀏覽器支持<canvas>元素
if (drawing.getContext){
    var context = drawing.getContext("2d"),
        image = document.images[0],
        imageData, data,
        i, len, average,
        red, green, blue, alpha;

    // 繪制原始圖像
    context.drawImage(image, 0, 0);

    // 取得圖像數(shù)據(jù)
    imageData = context.getImageData(0, 0, image.width, image.height);
    data = imageData.data;

    for (i=0, len=data.length; i < len; i+=4){
        red = data[i];
        green = data[i+1];
        blue = data[i+2];
        alpha = data[i+3];

        // 求得 rgb 平均值
        average = Math.floor((red + green + blue) / 3);

        // 設(shè)置顏色值,透明度不變
        data[i] = average;
        data[i+1] = average;
        data[i+2] = average;
    }

    // 回寫圖像數(shù)據(jù)并顯示結(jié)果
    imageData.data = data;
    context.putImageData(imageData, 0, 0);
} 

??上述例子首先在畫面上繪制了一幅圖像,然后取得了原始圖像數(shù)據(jù)。其中的 for 循環(huán)遍歷了圖像數(shù)據(jù)中的每一個(gè)像素。這里要注意的是,每次循環(huán)控制變量 i 都遞增 4。
??在取得每個(gè)像素的紅、綠、藍(lán)顏色值后,計(jì)算出它們的平均值。再把這個(gè)平均值設(shè)置為每個(gè)顏色的值,結(jié)果就是去掉了每個(gè)像素的顏色,只保留了亮度接近的灰度值(即彩色變黑白)。
??在把 data 數(shù)組回寫到 imageData 對象后,調(diào)用putImageData() 方法把圖像數(shù)據(jù)繪制到畫布上。最終得到了圖像的黑白版。
??當(dāng)然,通過操作原始像素值不僅能實(shí)現(xiàn)灰階過濾,還能實(shí)現(xiàn)其他功能。

??只有在畫布“干凈”的情況下(即圖像并非來自其他域),才可以取得圖像數(shù)據(jù)。如果畫布“不干凈”,那么訪問圖像數(shù)據(jù)時(shí)會(huì)導(dǎo)致 JavaScript 錯(cuò)誤。

2.11、合成

??還有兩個(gè)會(huì)應(yīng)用到 2D 上下文中所有繪制操作的屬性:globalAlpha 和 globalCompositionOperation。
??其中,globalAlpha 是一個(gè)介于 0 和 1 之間的值(包括 0 和 1),用于指定所有繪制的透明度。默認(rèn)值為 0。如果所有后續(xù)操作都要基于相同的透明度,就可以先把 globalAlpha 設(shè)置為適當(dāng)值,然后繪制,最后再把它設(shè)置回默認(rèn)值 0。示例:

// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);

// 修改全局透明度
context.globalAlpha = 0.5;

// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0, 0, 255, 1)";
context.fillRect(30, 30, 50, 50);

// 重置全局透明度
context.globalAlpha = 0; 

??在上述例子中,我們把藍(lán)色矩形繪制到了紅色矩形上面。因?yàn)樵诶L制藍(lán)色矩形前,globalAlpha 已經(jīng)被設(shè)置為 0.5,所以藍(lán)色矩形會(huì)呈現(xiàn)半透明效果,透過它可以看到下面的紅色矩形。

??第二個(gè)屬性 globalCompositionOperation 表示后繪制的圖形怎樣與先繪制的圖形結(jié)合。這個(gè)屬性的值是字符串,可能的值如下。

  • source-over(默認(rèn)值):后繪制的圖形位于先繪制的圖形上方。
  • source-in:后繪制的圖形與先繪制的圖形重疊的部分可見,兩者其他部分完全透明。
  • source-out:后繪制的圖形與先繪制的圖形不重疊的部分可見,先繪制的圖形完全透明。
  • source-atop:后繪制的圖形與先繪制的圖形重疊的部分可見,先繪制圖形不受影響。
  • destination-over:后繪制的圖形位于先繪制的圖形下方,只有之前透明像素下的部分才可見。
  • destination-in:后繪制的圖形位于先繪制的圖形下方,兩者不重疊的部分完全透明。
  • destination-out:后繪制的圖形擦除與先繪制的圖形重疊的部分。
  • destination-atop:后繪制的圖形位于先繪制的圖形下方,在兩者不重疊的地方,先繪制的圖形會(huì)變透明。
  • lighter:后繪制的圖形與先繪制的圖形重疊部分的值相加,使該部分變亮。
  • copy:后繪制的圖形完全替代與之重疊的先繪制圖形。
  • xor:后繪制的圖形與先繪制的圖形重疊的部分執(zhí)行“異或”操作。

??這個(gè)合成操作實(shí)際上用語言或者黑白圖像是很難說清楚的。要了解每個(gè)操作的具體效果,請參見 https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html。推薦使用 IE9+或 Firefox 4+訪問前面的網(wǎng)頁,因?yàn)檫@兩款瀏覽器對 Canvas 的實(shí)現(xiàn)最完善。下面來看一個(gè)例子。

// 繪制紅色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);

// 設(shè)置合成操作
context.globalCompositeOperation = "destination-over";

// 繪制藍(lán)色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);

??如果不修改 globalCompositionOperation,那么藍(lán)色矩形應(yīng)該位于紅色矩形之上。但把 globalCompositionOperation 設(shè)置為"destination-over"之后,紅色矩形跑到了藍(lán)色矩形上面。
??在使用 globalCompositionOperation 的情況下,一定要多測試一些瀏覽器。因?yàn)椴煌瑸g覽器對這個(gè)屬性的實(shí)現(xiàn)仍然存在較大的差別。Safari 和 Chrome 在這方面還有問題,至于有什么問題,大家可以比較在打開上述頁面的情況下,IE9+和 Firefox 4+與它們有什么差異。

小結(jié)

??HTML5 的<canvas>元素提供了一組 JavaScript API,讓我們可以動(dòng)態(tài)地創(chuàng)建圖形和圖像。圖形是在一個(gè)特定的上下文中創(chuàng)建的,而上下文對象目前有兩種。第一種是 2D 上下文,可以執(zhí)行原始的繪圖操作,比如:

  • 設(shè)置填充、描邊顏色和模式
  • 繪制矩形
  • 繪制路徑
  • 繪制文本
  • 創(chuàng)建漸變和模式

??第二種是 3D 上下文,即 WebGL 上下文。WebGL 是從 OpenGL ES 2.0 移植到瀏覽器中的,而 OpenGLES 2.0 是游戲開發(fā)人員在創(chuàng)建計(jì)算機(jī)圖形圖像時(shí)經(jīng)常使用的一種語言。WebGL 支持比 2D 上下文更豐富和更強(qiáng)大的圖形圖像處理能力,比如:

  • 用 GLSL(OpenGL Shading Language,OpenGL 著色語言)編寫的頂點(diǎn)和片段著色器
  • 支持類型化數(shù)組,即能夠?qū)?shù)組中的數(shù)據(jù)限定為某種特定的數(shù)值類型
  • 創(chuàng)建和操作紋理

??目前,主流瀏覽器的較新版本大都已經(jīng)支持<canvas>標(biāo)簽。同樣地,這些版本的瀏覽器基本上也都支持 2D 上下文。但對于 WebGL 而言,目前還只有 Firefox 4+和 Chrome 支持它。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,725評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內(nèi)容

  • 本章內(nèi)容 理解 元素 繪制簡單的 2D 圖形 使用 WebGL 繪制 3D 圖形 這個(gè)元素負(fù)責(zé)在頁面中設(shè)定一個(gè)區(qū)域...
    悶油瓶小張閱讀 856評論 0 0
  • --繪圖與濾鏡全面解析 概述 在iOS中可以很容易的開發(fā)出絢麗的界面效果,一方面得益于成功系統(tǒng)的設(shè)計(jì),另一方面得益...
    韓七夏閱讀 2,747評論 2 10
  • Core Graphics Framework是一套基于C的API框架,使用了Quartz作為繪圖引擎。它提供了低...
    ShanJiJi閱讀 1,548評論 0 20
  • 遇見你,我就好像遇見了真正的自己 我說,你欠我很多擁抱 你說,我是你不能擁抱的短暫理想 那你就努力伸長手夠啊 我知...
    洛邀閱讀 491評論 0 0