當前位置: 妍妍網 > 碼農

實戰三個有趣小案例,10分鐘入門Canvas~

2024-03-31碼農

前言

大家好,我是林三心,回想起我當年校招的時候啊,多次被面試官問到 canvas ,但是我卻不會,後來一直想找個機會學一下 canvas ,但是一直沒時間。 canvas 在前端的地位是越來越重要了,為此,我特地寫了3個小計畫,讓你們10分鐘就能入門 canvas ,是的,我的心裏沒有她,只有你們

image.png

1. canvas實作時鐘轉動

實作以下效果,分為幾步:

  • 1、找到canvas的 中心 ,畫出 表心 ,以及 表框

  • 2、獲取 當前時間 ,並根據時間畫出 時針,分針,秒針 ,還有 刻度

  • 3、使用定時器,每過一秒 獲取新的時間 ,並重新繪圖,達到時鐘 轉動的效果

  • 螢幕擷取2021-07-19 下午8.52.15.png

    1.1 表心,表框

    畫表心,表框有兩個知識點:

  • 1、找到canvas的 中心位置

  • 2、繪制 圓形

  • //html
    <canvas id="canvas" width="600" height="600"></canvas>
    /
    / js
    /
    / 設定中心點,此時300,300變成了座標的0,0
    ctx.translate(300, 300)
    /
    / 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
    ctx.arc(0, 0, 100, 0, 2 * Math.PI)
    ctx.arc(0, 0, 5, 0, 2 * Math.PI)
    /
    / 執行畫線段的操作stroke
    ctx.stroke() 

    讓我們來看看效果,發現了,好像不對啊,我們是想畫 兩個獨立的圓線 ,怎麽畫出來的兩個圓 連到一起了

    原因是:上面程式碼畫連個圓時,是連著畫的,所以畫完大圓後,線還沒斬斷,就接著畫小圓,那肯定會大圓小圓連一起,解決辦法就是: beginPath,closePath

    ctx.translate(300300// 設定中心點,此時300,300變成了座標的0,0
    // 畫大圓
    + ctx.beginPath()
    // 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
    ctx.arc(0010002 * Math.PI)
    ctx.stroke() // 執行畫線段的操作
    + ctx.closePath()
    // 畫小圓
    + ctx.beginPath()
    ctx.arc(00502 * Math.PI)
    ctx.stroke()
    + ctx.closePath()

    1.2 時針,分針,秒針

    畫這三個指標,有兩個知識點:

  • 1、根據當前 時,分,秒 計算角度

  • 2、在計算好的角度上去畫出 時針,分針,秒針 如何根據算好的角度去畫線呢,比如算出當前是 3點 ,那麽時針就應該以 12點 為起始點, 順時針 旋轉 2 * Math.PI / 12 * 3 = 90° ,分針和秒針也是同樣的道理,只不過跟時針不同的是 比例問題 而已,因為 時在表上有12份 ,而 分針和秒針都是60份

  • 螢幕擷取2021-07-19 下午10.07.19.png

    這時候又有一個新問題,還是以上面的例子為例,我算出了 90° ,那我們怎麽畫出時針呢?我們可以使用 moveTo和lineTo 去畫線段。至於90°,我們只需要將 x軸 順時針旋轉 90° ,然後再畫出這條線段,那就得到了指定角度的指標了。但是上面說了,是要以 12點為起始點 ,我們的 預設x軸確是水平 的,所以我們分時秒針算出角度後,每次都要 減去90° 。可能這有點繞,我們透過下面的圖演示一下,還是以上面 3點 的例子:

    螢幕擷取2021-07-19 下午10.30.23.png

    這樣就得出了3點指標的畫線角度了。

    又又又有新問題了,比如現在我畫完了時針,然後我想畫分針,x軸已經在我畫時針的時候偏轉了,這時候肯定要讓x軸恢復到原來的模樣,我們才能繼續畫分針,否則畫出來的分針是不準的。這時候 save和restore 就派上用場了, save是把ctx當前的狀態打包壓入棧中,restore是取出棧頂的狀態並賦值給ctx save可多次,但是restore取狀態的次數必須等於save次數

    螢幕擷取2021-07-19 下午10.42.06.png

    懂得了上面所說,剩下畫 刻度 了,起始刻度的道理跟分時秒針道理一樣,只不過刻度是死的,不需要計算,只需要規則畫出 60個小刻度 ,和 12個大刻度 就行

    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.translate(300300// 設定中心點,此時300,300變成了座標的0,0
    // 把狀態保存起來
    + ctx.save()
    // 畫大圓
    ctx.beginPath()
    // 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
    ctx.arc(0010002 * Math.PI)
    ctx.stroke() // 執行畫線段的操作
    ctx.closePath()
    // 畫小圓
    ctx.beginPath()
    ctx.arc(00502 * Math.PI)
    ctx.stroke()
    ctx.closePath()
    ----- 新加程式碼 ------
    // 獲取當前 時,分,秒
    let time = newDate()
    let hour = time.getHours() % 12
    let min = time.getMinutes()
    let sec = time.getSeconds()
    // 時針
    ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
    ctx.beginPath()
    // moveTo設定畫線起點
    ctx.moveTo(-100)
    // lineTo設定畫線經過點
    ctx.lineTo(400)
    // 設定線寬
    ctx.lineWidth = 10
    ctx.stroke()
    ctx.closePath()
    // 恢復成上一次save的狀態
    ctx.restore()
    // 恢復完再保存一次
    ctx.save()
    // 分針
    ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-100)
    ctx.lineTo(600)
    ctx.lineWidth = 5
    ctx.stroke style = 'blue'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()
    //秒針
    ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-100)
    ctx.lineTo(800)
    ctx.stroke style = 'red'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()
    // 繪制刻度,也是跟繪制分時秒針一樣,只不過刻度是死的
    ctx.lineWidth = 1
    for (let i = 0; i < 60; i++) {
    ctx.rotate(2 * Math.PI / 60)
    ctx.beginPath()
    ctx.moveTo(900)
    ctx.lineTo(1000)
    // ctx.stroke style = 'red'
    ctx.stroke()
    ctx.closePath()
    }
    ctx.restore()
    ctx.save()
    ctx.lineWidth = 5
    for (let i = 0; i < 12; i++) {
    ctx.rotate(2 * Math.PI / 12)
    ctx.beginPath()
    ctx.moveTo(850)
    ctx.lineTo(1000)
    ctx.stroke()
    ctx.closePath()
    }
    ctx.restore()








    螢幕擷取2021-07-19 下午10.53.53.png

    最後一步就是更新檢視,使時鐘轉動起來,第一想到的肯定是定時器 setInterval ,但是註意一個問題:每次更新檢視的時候都要把上一次的畫布清除,再開始畫新的檢視,不然就會出現 千手觀音 的景象

    螢幕擷取2021-07-19 下午10.57.05.png

    附上最終程式碼:

    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    setInterval(() => {
    ctx.save()
    ctx.clearRect(00600600)
    ctx.translate(300300// 設定中心點,此時300,300變成了座標的0,0
    ctx.save()
    // 畫大圓
    ctx.beginPath()
    // 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
    ctx.arc(0010002 * Math.PI)
    ctx.stroke() // 執行畫線段的操作
    ctx.closePath()
    // 畫小圓
    ctx.beginPath()
    ctx.arc(00502 * Math.PI)
    ctx.stroke()
    ctx.closePath()
    // 獲取當前 時,分,秒
    let time = newDate()
    let hour = time.getHours() % 12
    let min = time.getMinutes()
    let sec = time.getSeconds()
    // 時針
    ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
    ctx.beginPath()
    // moveTo設定畫線起點
    ctx.moveTo(-100)
    // lineTo設定畫線經過點
    ctx.lineTo(400)
    // 設定線寬
    ctx.lineWidth = 10
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()
    // 分針
    ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-100)
    ctx.lineTo(600)
    ctx.lineWidth = 5
    ctx.stroke style = 'blue'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()
    //秒針
    ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-100)
    ctx.lineTo(800)
    ctx.stroke style = 'red'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()
    // 繪制刻度,也是跟繪制分時秒針一樣,只不過刻度是死的
    ctx.lineWidth = 1
    for (let i = 0; i < 60; i++) {
    ctx.rotate(2 * Math.PI / 60)
    ctx.beginPath()
    ctx.moveTo(900)
    ctx.lineTo(1000)
    // ctx.stroke style = 'red'
    ctx.stroke()
    ctx.closePath()
    }
    ctx.restore()
    ctx.save()
    ctx.lineWidth = 5
    for (let i = 0; i < 12; i++) {
    ctx.rotate(2 * Math.PI / 12)
    ctx.beginPath()
    ctx.moveTo(850)
    ctx.lineTo(1000)
    // ctx.stroke style = 'red'
    ctx.stroke()
    ctx.closePath()
    }
    ctx.restore()
    ctx.restore()
    }, 1000)







    效果 very good 啊:

    clock的副本.gif

    2. canvas實作刮刮卡

    小時候很多人都買過充值卡把,懂的都懂啊哈,用指甲刮開這層灰皮,就能看底下的答案了。

    思路是這樣的:

  • 1、底下答案是一個 div ,頂部灰皮是一個 canvas canvas 一開始蓋住 div

  • 2、滑鼠事件,點選時並移動時,滑鼠經過的路徑都 畫圓形 開路,並且設定 globalCompositeOperation destination-out ,使滑鼠經過的路徑都 變成透明 ,一透明,自然就顯示出下方的答案資訊。

  • 關於 fill 這個方法,其實是對標 stroke 的, fill 是把圖形填充, stroke 只是畫出邊框線

    // html
    <canvas id="canvas" width="400" height="100"></canvas>
    <div class="text">恭喜您獲得100w</
    div>
    < style>
    * {
    margin0;
    padding0;
    }
    .text {
    position: absolute;
    left130px;
    top35px;
    z-index: -1;
    }
    </ style>


    // js
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    // 填充的顏色
    ctx.fill style = 'darkgray'
    // 填充矩形 fillRect(起始X,起始Y,終點X,終點Y)
    ctx.fillRect(00400100)
    ctx.fill style = '#fff'
    // 繪制填充文字
    ctx.fillText('刮刮卡'18050)
    let isDraw = false
    canvas.onmousedown = function () {
    isDraw = true
    }
    canvas.onmousemove = function (e{
    if (!isDraw) return
    // 計算滑鼠在canvas裏的位置
    const x = e.pageX - canvas.offsetLeft
    const y = e.pageY - canvas.offsetTop
    // 設定globalCompositeOperation
    ctx.globalCompositeOperation = 'destination-out'
    // 畫圓
    ctx.arc(x, y, 1002 * Math.PI)
    // 填充圓形
    ctx.fill()
    }
    canvas.onmouseup = function () {
    isDraw = false
    }

    效果如下:

    guaguaka.gif

    3. canvas實作畫板和保存

    框架:使用 vue + elementUI

    其實很簡單,難點有以下幾點:

  • 1、滑鼠拖拽畫正方形和圓形

  • 2、畫完一個保存畫布,下次再畫的時候疊加

  • 3、保存圖片

  • 第一點,只需要計算出滑鼠點選的點座標,以及滑鼠的當前座標,就可以計算了,矩形長寬計算: x - beginX, y - beginY ,圓形則要利用勾股定理: Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))

    第二點,則要利用canvas的 getImageData putImageData 方法

    第三點,思路是將 canvas 生成圖片連結,並賦值給具有下載功能的 a標簽 ,並主動點選 a標簽 進行 圖片下載

    看看效果吧:

    螢幕擷取2021-07-19 下午11.16.24.png

    螢幕擷取2021-07-19 下午11.17.41.png

    具體程式碼我就不過多講解了,說難也不難,只要前面兩個計畫理解了,這個計畫很容易就懂了:

    <template>
    <div>
    <div style="margin-bottom: 10px; display: flex; align-items: center">
    <el-button @click="changeType('huabi')"type="primary">畫筆</el-button>
    <el-button @click="changeType('rect')"type="success">正方形</el-button>
    <el-button
    @click="changeType('arc')"
    type="warning"
    style="margin-right: 10px"
    >
    圓形</el-button
    >

    <div>顏色:</div>
    <el-color-pickerv-model="color"></el-color-picker>
    <el-button @click="clear">清空</el-button>
    <el-button @click="saveImg">保存</el-button>
    </div>
    <canvas
    id="canvas"
    width="800"
    height="400"
    @mousedown="canvasDown"
    @mousemove="canvasMove"
    @mouseout="canvasUp"
    @mouseup="canvasUp"
    >

    </canvas>
    </div>
    </template>

    <script>
    exportdefault {
    data() {
    return {
    type"huabi",
    isDrawfalse,
    canvasDomnull,
    ctxnull,
    beginX0,
    beginY0,
    color"#000",
    imageDatanull,
    };
    },
    mounted() {
    this.canvasDom = document.getElementById("canvas");
    this.ctx = this.canvasDom.getContext("2d");
    },
    methods: {
    changeType(type) {
    this.type = type;
    },
    canvasDown(e) {
    this.isDraw = true;
    const canvas = this.canvasDom;
    this.beginX = e.pageX - canvas.offsetLeft;
    this.beginY = e.pageY - canvas.offsetTop;
    },
    canvasMove(e) {
    if (!this.isDraw) return;
    const canvas = this.canvasDom;
    const ctx = this.ctx;
    const x = e.pageX - canvas.offsetLeft;
    const y = e.pageY - canvas.offsetTop;
    this[`${this.type}Fn`](ctx, x, y);
    },
    canvasUp() {
    this.imageData = this.ctx.getImageData(00800400);
    this.isDraw = false;
    },
    huabiFn(ctx, x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 502 * Math.PI);
    ctx.fill style = this.color;
    ctx.fill();
    ctx.closePath();
    },
    rectFn(ctx, x, y) {
    const beginX = this.beginX;
    const beginY = this.beginY;
    ctx.clearRect(00800400);
    this.imageData && ctx.putImageData(this.imageData, 0000800400);
    ctx.beginPath();
    ctx.stroke style = this.color;
    ctx.rect(beginX, beginY, x - beginX, y - beginY);
    ctx.stroke();
    ctx.closePath();
    },
    arcFn(ctx, x, y) {
    const beginX = this.beginX;
    const beginY = this.beginY;
    this.isDraw && ctx.clearRect(00800400);
    this.imageData && ctx.putImageData(this.imageData, 0000800400);
    ctx.beginPath();
    ctx.stroke style = this.color;
    ctx.arc(
    beginX,
    beginY,
    Math.round(
    Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
    ),
    0,
    2 * Math.PI
    );
    ctx.stroke();
    ctx.closePath();
    },
    saveImg() {
    const url = this.canvasDom.toDataURL();
    const a = document.createElement("a");
    a.download = "sunshine";
    a.href = url;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    },
    clear() {
    this.imageData = null
    this.ctx.clearRect(00800400)
    }
    },
    };
    </script>

    < stylelang="scss"scoped>
    #canvas {
    border1px solid black;
    }
    </ style>

    結語

    我是林三心

  • 一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;

  • 一個偏前端的全幹工程師;

  • 一個不正經的金塊作者;

  • 逗比的B站up主;

  • 不帥的小紅書博主;

  • 喜歡打鐵的籃球菜鳥;

  • 喜歡歷史的乏味少年;

  • 喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 --> 摸魚沸點

  • 廣州的兄弟可以約飯哦,或者約球~