Canvas 從進階到退學(xué)

本文簡介

點贊 + 關(guān)注 + 收藏 = 學(xué)會了


接著 《Canvas 從入門到勸朋友放棄(圖解版)》 坡脐,本文繼續(xù)補充 canvas 基礎(chǔ)知識點。

這次我不手繪了房揭!


本文會涉及到 canvas 的知識包括:變形备闲、像素控制、漸變捅暴、陰影恬砂、路徑



變形

這里說的變形是基于畫布,全局進行變形蓬痒。

變形主要包括:平移 translate泻骤、縮放 scale旋轉(zhuǎn)操作 rotate乳幸。

除了對應(yīng)的方法外瞪讼,還可以使用 transformsetTransform 對上面三種操作進行配置,這稱為“變換矩陣”粹断。


在學(xué)習(xí)“變形”之前符欠,需要了解 W3C坐標(biāo)系

file

箭頭所指是各軸自己的正方向,x軸越往右(正方向)值越大瓶埋,y軸越往下(正方向)值越大希柿。


平移

使用 translate() 方法可以實現(xiàn)平移效果(位移)。

translate(x, y) 接收2個參數(shù)养筒,第一個參數(shù)代表x軸方向位移距離曾撤,第二個參數(shù)代表y軸方向位移距離。

正數(shù)代表向正方向位移晕粪,負數(shù)代表向反方向位移挤悉。


演示平移效果之前,我先創(chuàng)建一個矩形巫湘,長寬都是100装悲,位置在畫布的原點 (0, 0) 昏鹃,也就是緊貼畫布的左上角。

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 緊貼原點的矩形诀诊,默認是黑色[]
  ctx.fillRect(0, 0, 100, 100)
</script>


如果此時在 fillRect 之前設(shè)置 translate 就可以實現(xiàn)整個畫布位移的效果洞渤。

file
// 省略部分代碼

// 平移,往右平移10属瓣,往下平移20
ctx.translate(10, 20)

// 渲染矩形
ctx.fillRect(0, 0, 100, 100)

從上圖可以看出载迄,矩形距離畫布頂部的距離是20,距離畫布左側(cè)的距離是10抡蛙。


注意:平移 translate() 要寫在 “繪制操作(本例是 fillRect)” 之前才有效护昧。


如果在使用 translate 的前后都有渲染操作,畫布會多次渲染粗截,并不會自動清屏捏卓。

比如這樣

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  ctx.translate(10, 20)

  ctx.fillRect(0, 0, 100, 100)
</script>


再做個明顯點的效果,每秒平移一次

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  setInterval(() => {
    ctx.translate(10, 20)
    ctx.fillRect(0, 0, 100, 100)
  }, 1000)

</script>

可以看出慈格,每次使用 translate() 平移畫布,都會基于上一次畫布所在的位置進行平移遥金。


上圖效果是 canvas 的默認效果浴捆,所以在執(zhí)行 translate 之前可以執(zhí)行 “清屏操作”


清屏

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  setInterval(() => {
    ctx.clearRect(0, 0, context.width, context.height)
    ctx.translate(10, 20)
    ctx.fillRect(0, 0, 100, 100)
  }, 1000)

</script>


縮放

縮放畫布用到的方法是 scale(x, y) 稿械,接收2個參數(shù)选泻,第一個參數(shù)是x軸方向的縮放,第二個參數(shù)是y軸方向的縮放美莫。

當(dāng) x 或者 y 的值是 0 ~ 1 時代表縮小页眯,比如取值為 0.5 時,表示比原本縮小一半厢呵;值為2時窝撵,比原本放大一倍。

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'hotpink'
  ctx.strokeText('雷猴', 40, 100)

  // 縮小
  ctx.scale(0.5, 0.5)
  
  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>

scale() 方法同樣會保留原本已經(jīng)渲染的內(nèi)容襟铭。

如果不需要保留原本內(nèi)容碌奉,可以使用 “清屏操作”

注意:scale() 會以上一次縮放為基準(zhǔn)進行下一次縮放寒砖。


副作用:

其實從上面的例子就可以看出 scale() 存在一點副作用的赐劣,從圖中可以看出,縮放后文本的左上角坐標(biāo)發(fā)生了“位移”哩都,文本描邊粗細也發(fā)生了變化魁兼。

雖然說是副作用,但也很容易理解漠嵌,整塊畫布縮放了咐汞,對應(yīng)的坐標(biāo)比例其實也跟著縮放嘛盖呼。


旋轉(zhuǎn)

使用 rotate(angle) 方法可以旋轉(zhuǎn)畫布,但默認的旋轉(zhuǎn)原點是畫布的左上角碉考,也就是 (0, 0) 坐標(biāo)塌计。

我計算旋轉(zhuǎn)角度通常是用 角度 * Math.PI / 180 的方式表示。

雖然這樣書寫代碼看上去很長侯谁,但習(xí)慣后就比較直觀的看出要旋轉(zhuǎn)多少度锌仅。

rotate(angle) 中的參數(shù) angle 代表角度,angle 的取值范圍是 -Math.PI * 2 ~ Math.pi * 2墙贱。

當(dāng)旋轉(zhuǎn)角度小于 0 時热芹,畫布逆時針旋轉(zhuǎn);反之順時針旋轉(zhuǎn)惨撇。

file
<canvas id="c" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'pink'
  ctx.strokeText('雷猴', 40, 100)

  // 旋轉(zhuǎn) 45°
  ctx.rotate(45 * Math.PI / 180)
  
  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>


修改原點

如果需要修改旋轉(zhuǎn)中心伊脓,可以使用 translate() 方法平移畫布,通過計算移動到指定位置魁衙。

file
<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'pink'
  ctx.strokeText('雷猴', 40, 100)

  // 設(shè)置旋轉(zhuǎn)中心
  ctx.translate(90, -50)

  // 旋轉(zhuǎn)
  ctx.rotate(45 * Math.PI / 180)
  
  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>


變換矩陣

變換矩陣常用方法有 transform()setTransform() 兩個方法报腔。

變換矩陣是一個稍微進階一點的知識了,別怕剖淀!

前面的 平移 translate纯蛾、縮放 scale旋轉(zhuǎn)操作 rotate 可以說都是 transform() 的 “語法糖”纵隔。

變換矩陣已經(jīng)涉及到一點數(shù)學(xué)知識了翻诉,但本文不會講到這些知識,只會講講 transform() 是怎么用的捌刮。


transform

transform() 一個方法就可以實現(xiàn) 平移碰煌、縮放、旋轉(zhuǎn) 三種功能绅作,它接收6個參數(shù)芦圾。

transform(a, b, c, d, e, f)

  • a: 水平縮放(x軸方向),默認值是 1俄认;
  • b: 水平傾斜(x軸方向)堕扶,默認值是 0兴溜;
  • c: 垂直傾斜(y軸方向)呀舔,默認值是 0怕午;
  • d: 垂直縮放(y軸方向)发皿,默認值是 1谷朝;
  • e: 水平移動(x軸方向)葫哗,默認值是 0括享;
  • f: 垂直移動(y軸方向)扣汪,默認值是 0;


這默認值看上去很亂科平,但如果這樣排列一下是不是就比較容易理解了:
\begin{pmatrix}a & c & e \\\\ b & d & f \\\\ 0 & 0 & 1 \end{pmatrix}


隨便修改幾個值試試效果:

file
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 變換矩陣
  ctx.transform(1, 1, 1, 2, 30, 40)

  // 繪制矩形
  ctx.fillRect(10, 10, 100, 100)
</script>


setTransform

setTransform(a, b, c, d, e, f) 同樣接收6個參數(shù)褥紫,和 transform() 一樣

file
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 變換矩陣
  ctx.setTransform(2, 1, 1, 2, 20, 10)

  // 繪制矩形
  ctx.fillRect(10, 10, 100, 100)
</script>


transform 和 setTransform 的區(qū)別

transform() 每次執(zhí)行都會參考上一次變換后的結(jié)果

比如下面這個多次執(zhí)行的情況:

file
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')
  
  ctx.fillStyle = 'rgba(10, 10, 10, 0.2)'
    
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

</script>


setTransform() 每次調(diào)用都會基于最原始是狀態(tài)進行變換。

file
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillStyle = 'rgba(10, 10, 10, 0.2)'

  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

</script>

不管改變多少次瞪慧,setTransform() 都會參考原始狀態(tài)進行變換髓考。



控制像素

位圖是由像素點組成的,canvas 提供了幾個 api 可以操作圖片中的像素弃酌。

很多工具網(wǎng)站也常用接下來說到的幾個 api 做圖片濾鏡氨菇。


需要注意的是,canvas 提供的操作像素的方法妓湘,必須使用服務(wù)器才能運行起來查蓉,不然沒有效果的。

可以搭建本地服務(wù)器運行本文案例榜贴,方法有很多種豌研。

比如你使用 Vue 或者 React 的腳手架搭建的項目,運行后就能跑起本文所有案例唬党。

又或者使用 http-server 啟動本地服務(wù)鹃共。


getImageData()

首先要介紹的是 getImageData() 方法,這個方法可以獲取指定區(qū)域內(nèi)的所有像素驶拱。


getImageData(x, y, width, height) 接收4個參數(shù)及汉,這4個參數(shù)表示選區(qū)范圍。

xy 代表選區(qū)的左上角坐標(biāo)屯烦,width 表示選區(qū)寬度,height 表示選區(qū)高度房铭。


還是舉例說明比較清楚驻龟。下圖渲染到畫布上的是我的貓Bubble

file
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 創(chuàng)建圖片對象
  img.src = './bubble.jpg' // 加載本地圖片

  // 圖片加載完成后在執(zhí)行其他操作
  img.onload = () => {
    // 渲染圖片
    ctx.drawImage(img, 0, 0)
    // 獲取圖片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)
    console.log(imageData)
  }

</script>

打印出來的信息可以點開大圖看看

  • data: 圖片像素數(shù)據(jù)集缸匪,以數(shù)組的形式存放翁狐,這是本文要講的重點,需要關(guān)注凌蔬!
  • colorSpace: 圖片使用的色彩標(biāo)準(zhǔn)露懒,這個屬性在 Chrome 里有打印出來,Firefox 里沒打印砂心。不重要~
  • height: 圖片高度
  • width: 圖片寬度


通過 getImageData() 獲取到的信息中懈词,需要重點關(guān)注的是 data ,它是一個一維數(shù)組辩诞,仔細觀察發(fā)現(xiàn)里面的值沒一個是大于255的坎弯,也不會小于0。

file

其實 data 屬性里記錄了圖片每個像素的 rgba 值分別是多少。

  • r 代表紅色
  • g 代表綠色
  • b 代表藍色
  • a 透明度


這個和 CSS 里的 rgba 是同一個意思抠忘。

data 里撩炊,4個元素記錄1個像素的信息。也就是說崎脉,1個像素是由 r拧咳、gb囚灼、a 4個元素組成骆膝。而且每個元素的取值范圍是 0 - 255 的整數(shù)。

 data: **[r1, g1, b1, a1, r2, g2, b2, a2, ......]** 
像素點 顏色通道
imgData.data[0] 49 紅色 r
imgData.data[1] 47 綠色 g
imgData.data[2] 51 藍色 b
imgData.data[3] 255 透明度 a
…… …… ……
imgData.data[n-4] 206 紅色 r
imgData.data[n-2] 200 綠色 g
imgData.data[n-3] 200 藍色 b
imgData.data[n-1] 255 透明度 a


如果一張圖只有10個像素啦撮,通過 getImageData() 獲取到的 data 信息中就有40個元素谭网。


putImageData()

putImageData(imageData, x, y) 可以將 ImageData 對象的數(shù)據(jù)(圖片像素數(shù)據(jù))繪制到畫布上。

putImageData(imgData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) 也可以接收更多參數(shù)赃春。

  • imageData: 規(guī)定要放回畫布的 ImageData 對象
  • x: ImageData 對象左上角的 x 坐標(biāo)愉择,以像素計
  • y: ImageData 對象左上角的 y 坐標(biāo),以像素計
  • dirtyX: 可選织中。水平值(x)锥涕,以像素計,在畫布上放置圖像的位置
  • dirtyY: 可選狭吼。水平值(y)层坠,以像素計,在畫布上放置圖像的位置
  • dirtyWidth: 可選刁笙。在畫布上繪制圖像所使用的寬度
  • dirtyHeight: 可選破花。在畫布上繪制圖像所使用的高度


比如,我要將圖片復(fù)制到另一個位置

file
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 創(chuàng)建圖片對象
  img.src = './bubble.jpg' // 加載本地圖片

  // 圖片加載完成后在執(zhí)行其他操作
  img.onload = () => {
    // 渲染圖片
    ctx.drawImage(img, 0, 0)
    // 獲取圖片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)

    // 將圖片對象輸出到 (100, 100) 的位置上
    ctx.putImageData(imageData, 100, 100)
  }

</script>

可以實現(xiàn)復(fù)制的效果疲吸。


透明

知道前面兩個 api 就可以實現(xiàn)透明效果了座每。

前面講到,通過 getImageData() 獲取的是一個數(shù)組類型的數(shù)據(jù)摘悴,每4個元素代表1個像素峭梳,就是rgba,而 a 表示透明通道蹂喻,所以只需修改每組像素的最后1個元素的值葱椭,就能修改圖片的不透明度。

file
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 創(chuàng)建圖片對象
  img.src = './bubble.jpg' // 加載本地圖片

  // 圖片加載完成后在執(zhí)行其他操作
  img.onload = () => {
    // 渲染圖片
    ctx.drawImage(img, 0, 0)
    // 獲取圖片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)

    for (let i = 0; i < imageData.data.length; i += 4) {
      imageData.data[i + 3] = imageData.data[i + 3] * 0.5
    }

    // 將圖片對象輸出到 (100, 100) 的位置上
    ctx.putImageData(imageData, 100, 100)
  }

</script>


濾鏡

要做不同的濾鏡效果口四,其實就是通過不同的算法去操作每個像素的值孵运,我在 《Canvas 10款基礎(chǔ)濾鏡(原理篇)》 講到相關(guān)知識,有興趣的工友可以點進去看看



漸變

csssvg 里都有漸變蔓彩,canvas 肯定也不會缺失這個能力啦掐松。

canvas 提供了 線性漸變 createLinearGradient徑向漸變 createRadialGradient踱侣。


線性漸變 createLinearGradient

canvas 中使用線性漸變步驟如下:

  1. 創(chuàng)建線性漸變對象: createLinearGradient(x1, y1, x2, y2)
  2. 添加漸變顏色: addColorStop(stop, color)
  3. 設(shè)置填充色或描邊顏色: fillStylestrokeStyle


createLinearGradient(x1, y1, x2, y2)

createLinearGradient(x1, y1, x2, y2) 中,x1, y1 表示漸變的起始位置大磺,x2, y2 表示漸變的結(jié)束位置抡句。

比如水平方向的從左往右的線性漸變,此時的 y1y2 的值是一樣的杠愧。

file

兩個點就可以確定一個漸變方向待榔。


addColorStop(stop, color)

addColorStop(stop, color) 方法可以添加漸變色。

第一個參數(shù) stop 表示漸變色位置的偏移量流济,取值范圍是 0 ~ 1锐锣。

第二個參數(shù) color 表示顏色。


填充漸變

實際編碼演示一下

file
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 1. 創(chuàng)建線性漸變對象
  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  // 2. 添加漸變顏色
  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  // 設(shè)置填充色
  ctx.fillStyle = lgrd

  // 創(chuàng)建矩形绳瘟,填充
  ctx.fillRect(10, 10, 200, 200)
</script>


如果想修改漸變的方向雕憔,只需在使用 createLinearGradient() 時設(shè)置好起點和終點坐標(biāo)即可。


除了填充色糖声,描邊漸變和文本漸變同樣可以做到斤彼。

描邊漸變

file
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  ctx.strokeStyle  = lgrd
  ctx.lineWidth = 10
  ctx.strokeRect(10, 10, 200, 200)

</script>


文本漸變

file
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  const text = '雷猴'
  ctx.font = 'bold 100px 黑體'
  ctx.fillStyle = lgrd
  ctx.fillText(text, 10, 100)
</script>


多色線性漸變

在 0 ~ 1 的范圍內(nèi),addColorStop 可以設(shè)置多個顏色在不同的位置上蘸泻。

file
// 省略部分代碼
lgrd.addColorStop(0, '#2a9d8f') // 綠色
lgrd.addColorStop(0.5, '#e9c46a') // 黃色
lgrd.addColorStop(1, '#f4a261') // 橙色


徑向漸變 createRadialGradient

徑向漸變是從一個點到另一個點擴散出去的漸變琉苇,是圓形(橢圓也可以)漸變。

直接看效果

file
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const rgrd = ctx.createRadialGradient(70, 70, 0, 70, 70, 60)
  rgrd.addColorStop(0, 'yellow')
  rgrd.addColorStop(1, 'pink')
  ctx.fillStyle = rgrd

  ctx.fillRect(10, 10, 120, 120)
</script>

createRadialGradient 可以創(chuàng)建一個徑向漸變的對象悦施。使用步驟和 createLinearGradient 一樣并扇,但參數(shù)不同。

createRadialGradient(x1, y1, r1, x2, y2, r2)

  • x1, y1: 漸變開始的圓心坐標(biāo)
  • r1: 漸變開始的圓心半徑
  • x2, y2: 漸變結(jié)束的圓心坐標(biāo)
  • r2: 漸變結(jié)束的圓心半徑


同樣使用 addColorStop 設(shè)置漸變顏色抡诞,同樣支持多色漸變穷蛹。


漸變的注意事項

漸變的定位坐標(biāo)是參照畫布的,超出定位的部分會使用最臨近的那個顏色昼汗。

我用線性漸變舉例肴熏。

file
<canvas id="c" width="600" height="600" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(200, 0, 400, 400)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  ctx.fillStyle = lgrd

  ctx.fillRect(10, 10, 160, 160)

  ctx.fillRect(220, 10, 160, 160)

  ctx.fillRect(430, 10, 160, 160)

  ctx.fillRect(10, 210, 160, 160)

  ctx.fillRect(220, 210, 160, 160)

  ctx.fillRect(430, 210, 160, 160)

  ctx.fillRect(10, 430, 160, 160)

  ctx.fillRect(220, 430, 160, 160)

  ctx.fillRect(430, 430, 160, 160)

</script>

上面的例子中,我只創(chuàng)建了一個漸變乔遮,然后創(chuàng)建了9個正方形。

此時正方形的填充色取決于出現(xiàn)在畫布中的位置取刃。

可以修改一下 createLinearGradient() 的定位數(shù)據(jù)對照理解蹋肮。

file
// 省略部分代碼
const lgrd = ctx.createLinearGradient(200, 0, 400, 400)


如果想每個圖形都有自己的漸變色,這需要定制化配置璧疗,每個創(chuàng)建每個圖形之前都單獨創(chuàng)建一個漸變色坯辩。

file
<canvas id="c" width="600" height="600" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 粉 - 黃 漸變
  const lgrd1 = ctx.createLinearGradient(10, 10, 160, 160)
  lgrd1.addColorStop(0, 'pink')
  lgrd1.addColorStop(1, 'yellow')
  ctx.fillStyle = lgrd1
  ctx.fillRect(10, 10, 160, 160)

  // 橘黃 - 藍紫 漸變
  const lgrd2 = ctx.createLinearGradient(210, 10, 380, 160)
  lgrd2.addColorStop(0, 'bisque')
  lgrd2.addColorStop(1, 'blueviolet')
  ctx.fillStyle = lgrd2
  ctx.fillRect(220, 10, 160, 160)
</script>


所以不管是填充色還是秒變顏色,每個元素最好都自己重新設(shè)定一下崩侠。不然可能會出現(xiàn)意想不到的效果~



陰影

陰影在前端也是很常用的特效漆魔。 依稀記得當(dāng)年還用 png 做陰影效果

canvas 中,和陰影相關(guān)的屬性主要有以下4個:

  • shadowOffsetX: 設(shè)置或返回陰影與形狀的水平距離改抡。默認值是0矢炼。大于0時向正方向偏移。
  • shadowOffsetY: 設(shè)置或返回陰影與形狀的垂直距離阿纤。默認值是0句灌。大于0時向正方向偏移。
  • shadowColor: 設(shè)置或返回用于陰影的顏色欠拾。 默認黑色胰锌。
  • shadowBlur: 設(shè)置或返回用于陰影的模糊級別。 默認值是0藐窄,數(shù)值越大模糊度越強资昧。


相信使用過 css 陰影屬性的工友,理解起 canvas 陰影也會非常輕松荆忍。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.shadowOffsetX = 10 // x軸偏移量
  ctx.shadowOffsetY = 10 // y軸偏移量
  ctx.shadowColor = '#f38181' // 陰影顏色
  ctx.shadowBlur = 10 // 陰影模糊度格带,默認0

  ctx.fillStyle = '#fce38a' // 填充色
  ctx.fillRect(30, 30, 200, 100)

  console.log(ctx.shadowOffsetX) // 輸出陰影x軸方向的偏移量:10
</script>


除了圖形外,文本和圖片都可以設(shè)置陰影效果东揣。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.shadowOffsetX = 10 // x軸偏移量
  ctx.shadowOffsetY = 10 // y軸偏移量
  ctx.shadowColor = '#b83b5e' // 陰影顏色
  ctx.shadowBlur = 10 // 陰影模糊度践惑,默認0

  const text = '雷猴'
  ctx.font = 'bold 100px 黑體'
  ctx.fillStyle = '#fce38a'
  ctx.fillText(text, 10, 100)
</script>



路徑

Canvas 從入門到勸朋友放棄(圖解版) —— 新開路徑 中我講到 新開路徑關(guān)閉路徑 的用法,本節(jié)會在上篇的基礎(chǔ)上豐富更多使用細節(jié)嘶卧。

本節(jié)要講的是

  • beginPath(): 新開路徑
  • closePath(): 關(guān)閉路徑
  • isPointInPath(): 判斷某個點是否在當(dāng)前路徑內(nèi)


beginPath()

beginPath() 方法是用來開辟一條新的路徑尔觉,這個方法會將當(dāng)前路徑之中的所有子路徑都清除掉,以此來重置當(dāng)前路徑芥吟。


如果你的畫布上有幾個基礎(chǔ)圖形(直線侦铜、多邊形、圓形钟鸵、弧钉稍、貝塞爾曲線),如果樣式相互之間受到影響棺耍,那你可以立刻想想在繪制新圖形之前是不是忘了使用 beginPath() 贡未。

先舉幾個例子說明一下。


污染:顏色蒙袍、線條粗細受到污染

后面的樣式覆蓋了前面的樣式俊卤。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一條線,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描邊
  ctx.stroke()

  // 第二條線害幅,紅色
  ctx.moveTo(50, 80)
  ctx.lineTo(150, 80)
  ctx.strokeStyle = 'red' // 紅色描邊
  ctx.lineWidth = 10 // 表面粗細
  ctx.stroke()
</script>


污染:圖形路徑污染

比如畫布上有一條直線和一個圓形消恍,不使用 beginPath() 開辟新路徑的話,它們可能會“打架”以现。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一條線狠怨,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描邊
  ctx.stroke()

  // 圓形
  ctx.arc(150, 120, 40, 0, 360 * Math.PI / 180)
  ctx.lineWidth = 4
  ctx.stroke()
</script>

明明直線和圓形是沒有交集的约啊,為什么會有一條傾斜的線把兩個元素連接起來?


解決辦法

除了上面兩種情況外佣赖,可能還有其他更加奇怪的情況(像極喝醉了假酒)恰矩,都可以先考慮是不是要使用 beginPath()

比如這樣做茵汰。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一條線枢里,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描邊
  ctx.lineWidth = 10
  ctx.stroke()

  // 圓形
  ctx.beginPath() // 開辟新的路徑
  ctx.arc(150, 120, 40, 0, 360 * Math.PI / 180)
  ctx.strokeStyle = 'skyblue' // 藍色描邊
  ctx.lineWidth = 4
  ctx.stroke()
</script>

在使用 arc 或者 moveTo 方法之前加上一句 ctx.beginPath() 就可以有效解決以上問題。

這個例子中蹂午,如果沒用 ctx.beginPath() 栏豺,canvas 就會以為 線 和 圓形 都屬于同一個路徑,所以在畫圓形時豆胸,下筆的時候就會把線的“結(jié)束點”和圓的“起點”相連起來奥洼。


stroke()fill() 都是以最近的 beginPath() 后面所定義的狀態(tài)樣式為基礎(chǔ)進行繪制的。


注意事項

前面的樣式會覆蓋后面元素的默認樣式晚胡,即使使用了 beginPath() 灵奖。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一條線,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描邊
  ctx.lineWidth = 10 // 表面粗細
  ctx.stroke()

  // 第二條線估盘,紅色
  ctx.beginPath()
  ctx.moveTo(50, 80)
  ctx.lineTo(150, 80)
  ctx.stroke()
</script>

第一條先設(shè)置了 strokeStylelineWidth 瓷患,第二條線并沒有設(shè)置這兩個屬性,即使在繪制第二條線的開始時使用了 ctx.beginPath() 遣妥,第二條線也會使用第一條線的 strokeStylelineWidth 擅编。除非第二條線自己也有設(shè)置這兩個屬性,不然就會沿用之前的配置項箫踩。


"特殊情況"

還要補充一個 “特殊情況”爱态。

file
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const ctx = cnv.getContext('2d')

  // 第一條線,粉色
  ctx.moveTo(50, 30)
  ctx.lineTo(150, 30)
  ctx.strokeStyle = 'pink' // 粉色描邊
  ctx.lineWidth = 10 // 描邊粗細
  ctx.stroke()

  // 矩形
  ctx.strokeStyle = 'red' // 紅色描邊
  ctx.strokeRect(50, 50, 200, 100)
</script>

這個例子中境钟,繪制矩形 rect 前并沒有用 beginPath() 锦担,但矩形的紅色描邊并沒有影響直線的粉色描邊。

其實還不止 strokeRect() 慨削,還有 fillRect()洞渔、strokeText()fillText() 都不會影響其他圖形缚态,這些方法都只會繪制圖形磁椒,不會影響原本路徑。


closePath()

closePath() 方法可以關(guān)閉當(dāng)前路徑猿规,它可以顯示封閉某段開放的路徑衷快。這個方法常用于關(guān)閉圓弧路徑或者由圓弧宙橱、線段創(chuàng)建出來的開放路徑姨俩。

closePath() 是關(guān)閉路徑蘸拔,并不是結(jié)束路徑。

關(guān)閉路徑环葵,指的是連接起點與終點调窍,也就是能夠自動封閉圖形。

結(jié)束路徑张遭,指的是開始新的路徑邓萨。


基礎(chǔ)用法

舉個例子會更直觀

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.lineTo(150, 140)
  ctx.stroke()
</script>

上面的代碼通過 moveTolineTo 畫了3個點,使用 stroke() 方法把這3個點連起來菊卷,就形成了上圖效果缔恳。

但如果此時在 stroke() 前使用 closePath() 方法,最終出來的路徑將自動閉合(將起點和終點連接起來)洁闰。

file
<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.lineTo(150, 140)
  ctx.closePath() // 關(guān)閉路徑
  ctx.stroke()
</script>


注意事項

看到上面的例子后歉甚,可能有些工友會覺得使用 ctx.lineTo(50, 40) 連接回起點也有同樣效果。

// 省略部分代碼
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40)
ctx.stroke()

確實在描邊為1像素時看上去效果是差不多的扑眉,但如果此時將 lineWidth 的值設(shè)置得大一點纸泄,就能看到明顯區(qū)別。

file
// 省略部分代碼
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40) // 連接回起點
ctx.stroke()

如果用 closePath() 自動閉合路徑的話腰素,會出現(xiàn)以下效果

file
// 省略部分代碼
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.closePath() // 關(guān)閉路徑
ctx.stroke()



本文到此就完結(jié)了聘裁,但 canvas 的知識點還沒完,還有很多很多弓千,根本學(xué)不完的那種衡便。

接下來 本專欄 的文章會偏向于 知識點 + 案例 的方式講解 canvas



代碼倉庫

?雷猴 Canvas



推薦閱讀

??《Canvas 從入門到勸朋友放棄(圖解版)》

??《Canvas 10款基礎(chǔ)濾鏡(原理篇)》

??《Fabric.js 從入門到膨脹》

??《『Three.js』起飛计呈!》

??《p5.js 光速入門》

??《SVG 從入門到后悔砰诵,怎么不早點學(xué)起來(圖解版)》


點贊 + 關(guān)注 + 收藏 = 學(xué)會了
代碼倉庫

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捌显,隨后出現(xiàn)的幾起案子茁彭,更是在濱河造成了極大的恐慌,老刑警劉巖扶歪,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件理肺,死亡現(xiàn)場離奇詭異,居然都是意外死亡善镰,警方通過查閱死者的電腦和手機妹萨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炫欺,“玉大人乎完,你說我怎么就攤上這事∑仿澹” “怎么了树姨?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵摩桶,是天一觀的道長。 經(jīng)常有香客問我帽揪,道長硝清,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任转晰,我火速辦了婚禮芦拿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘查邢。我一直安慰自己蔗崎,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布扰藕。 她就那樣靜靜地躺著蚁趁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪实胸。 梳的紋絲不亂的頭發(fā)上他嫡,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音庐完,去河邊找鬼钢属。 笑死,一個胖子當(dāng)著我的面吹牛门躯,可吹牛的內(nèi)容都是我干的淆党。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼讶凉,長吁一口氣:“原來是場噩夢啊……” “哼染乌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起懂讯,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤荷憋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后褐望,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勒庄,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年瘫里,在試婚紗的時候發(fā)現(xiàn)自己被綠了实蔽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡谨读,死狀恐怖局装,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤铐尚,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布阶冈,位于F島的核電站,受9級特大地震影響塑径,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜填具,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一统舀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧劳景,春花似錦誉简、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至筋量,卻和暖如春烹吵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桨武。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工肋拔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呀酸。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓凉蜂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親性誉。 傳聞我的和親對象是個殘疾皇子窿吩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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