畫板drawing board-六角學院Javascript新手地下城

畫板drawing board-六角學院Javascript新手地下城

這座山終於快爬完一半了…不得不說如果真的不知道該怎麼做時參考網路上類似的範例是一個還不錯的選擇,不然你可能卡在路上半天還在迷路…

Photo by pixabay

BOSS 弱點

【特定技術】遊戲規則

  1. 繪圖區請使用 Canvas 來設計,上方的控制列與下方的畫筆調整可不用
  2. SAVE :點擊後可直接下載轉出的 PNG 圖片
  3. CLEAR ALL:清除畫版樣式
  4. UNDO、REDO:上一步、下一步
  5. 點擊箭頭時,功能列介面皆可進行收闔
  6. 【擴充功能】請再自行增加「兩個功能」,我相信勇者們都是很有梗的~

Canvas

DOM vs Canvas

DOM在繪製主要是透過瀏覽器的graphic api來做,因為實際的細節是由瀏覽器處理所以操作上較為簡單,但是DOM的操作是很耗內存的,DOM越多內存的消耗量越大,而canvas雖然操作上較為困難但是速度快且善於處理大量的元素

要使用時在html內加上canvas,之後js透過id做canvas的相關設定

HTML

  <canvas id="canvas-panel"></canvas>

Javascript

    // 基礎設定
   function canvas_setting() {
      //獲取2d畫筆物件
      if (this.canvas.getContext) {
        this.canvas.width = window.innerWidth; //設定canvas寬度為畫面寬度
        this.canvas.height = window.innerHeight; //設定canvas高度為畫面高度
        this.ctx = this.canvas.getContext("2d"); //獲取canvas 2d畫筆
        this.ctx.lineWidth = this.line_width; //設定線條粗細
        this.ctx.strokeStyle = this.pen_color; //設定線條顏色
      } else {
        alert("Browser not support canvas");
      }
    }
    //繪畫設定 
    function drawImg(x1, y1, x2, y2) {//畫圖時需要畫筆以及兩點的x,y座標
        //設定線條的端點如何繪製,round為端點帶有一個半圓形的線蓋
        if (this.ctx.lineCap !== "round") {
          this.ctx.lineCap = "round";
        }
        // 設定線條與線條之間如何連結,round為圓弧形連接
        if (this.ctx.lineJoin !== "round") {
          this.ctx.lineJoin = "round";
        }
        this.ctx.beginPath(); //開始繪製   
        this.ctx.moveTo(x1, y1); //設定開始位置
        this.ctx.lineTo(x2, y2); //設定移動位置
        this.ctx.closePath(); //關閉路徑
        this.ctx.stroke(); //路徑上色
    }

Mouse Event

Mouse Event是完成畫板基本繪畫功能的一個非常重要的事件,要完成基本繪畫功能需要下列三個事件監聽

  1. mousedown - 滑鼠壓下
  2. mousemove - 滑鼠移動
  3. mouseup - 滑鼠放開
    this.canvas.addEventListener("mousedown", (e) => {
      //buttons為1表示為按下的滑鼠按鍵為左鍵-不讓其他鍵觸發事件
      if (e.buttons === 1) {
        this.pos_x = e.clientX; //抓取滑鼠水平位置
        this.pos_y = e.clientY; //抓取滑鼠垂直位置
        this.drawing = true;
      }
    });
    this.canvas.addEventListener("mousemove", (e) => {
      // 要畫成線需要兩組x、y座標
      this.drawImg(this.pos_x,this.pos_y,e.clientX,e.clientY);
      this.pos_x = e.clientX;
      this.pos_y = e.clientY;
    });
    // 滑鼠移開時畫筆位置重置,drawing狀態為false
    this.canvas.addEventListener("mouseup", (e) => {
      // 避免繪畫時有放開其他滑鼠按鍵的狀況
      if (e.buttons !== 1) {
        this.pos_x = 0; //抓取滑鼠水平位置歸0
        this.pos_y = 0; //抓取滑鼠垂直位置歸0
        this.drawing = false;
        //儲存當前的狀態方便用於上一步、下一步
        this.step += 1;  //歷史紀錄步驟+1
        this.record_Arr.push(this.canvas.toDataURL()); //將資料存成base64編碼後放進陣列,之後上一步、下一步會用到
      }
    });

Data URI

Data URI在網頁上的作用就是透過將圖片轉換為文字編碼的方式直接儲存在HTML、CSS內來降低Http請求次數(一張圖片就是一個請求)進而增進網頁載入效能的一個方式,但是同時他也有著無法快取、可讀性差、檔案變大(大約33%)以及如果圖檔有變化時需要重新編碼的缺點,所以要使用的話還是要依照當下狀況來看…,在這一題裡Data URI是用作「下載圖片」以及繪畫時的「上一步、下一步」用

Data URI格式

  data:[<mediatype>][;base64],<data>

我們可以透過canvas內建的toDataURL將其轉換成Data URI後下載

 canvas.toDataURL(type, encoderOptions);
 // type為類型 ex: image/png(默認)、image/jpeg、image/webp...
 // encoderOptions為圖片品質範圍為0~1,指定圖片格式為image/jpeg 或 image/webp

程式碼

    // 下載圖片
    function downloadImg() {
      const dataUrl = this.canvas.toDataURL("image/png");
      document.querySelector(".save").href = dataUrl; //轉換完後丟給a標籤,之後只要點擊標籤就會下載
    }
    // 不論是上一步、下一步都需要先等待圖片載入完畢
    // 上一步
    function undo(){
      if (this.step > 0) {
        //先將狀態回到上一步
        var last_history = new Image();
        var window_width = window.innerWidth;
        var window_height = window.innerHeight;
        last_history.src = this.record_Arr[this.step-1]; //載入上一筆歷史紀錄
        last_history.onload = () => {
          this.ctx.clearRect(0, 0, window_width, window_height); //繪圖前必須先清除畫布
          this.ctx.drawImage(last_history, 0, 0); //載入圖片
          this.step-=1; //歷史紀錄步驟-1
        };
      }
    }
    // 下一步
    function redo_canvas() {
      // 如果有上一步操作才可以下一步
      if (this.step < this.record_Arr.length) {
        var last_history = new Image();
        var window_width = window.innerWidth;
        var window_height = window.innerHeight;
        last_history.src = this.record_Arr[this.step + 1]; //載入下一筆歷史紀錄
        last_history.onload = () => {
          this.ctx.clearRect(0, 0, window_width, window_height); //繪圖前必須先清除畫布
          this.ctx.drawImage(last_history, 0, 0); //載入圖片
          this.step +=1 ; //歷史紀錄步驟+1
        };
      }
    }

客製化功能

這次客製化了「顏色選取器」以及「橡皮擦」這兩個功能,我們直接來看程式碼

顏色選取器

  //選取色盤的顏色並變換畫筆顏色     
  function color_picker() {
    var color_picker = document.getElementById("color-panel"); //抓取type為color的input
    color_picker.addEventListener("input", (e)=> {
      this.selected_color = e.target.value; //抓取選取到的顏色
      document.querySelector('.colors .show').parentElement.style.backgroundColor = this.selected_color; //將原先勾選到的顏色改成目前選取的顏色
      this.pen_color = this.selected_color; //改變畫筆顏色
      this.ctx.strokeStyle = this.pen_color; //改變畫筆顏色
    });
  }

橡皮擦

橡皮擦原先的想法是想說將畫筆的顏色直接改成跟背景顏色一樣就可以了,但是png檔的背景是透明的,所以輸出的圖片會變成這樣 (灰色的部分為橡皮擦擦過的路徑)

所以單純改變畫筆顏色是不行的,但是我們可以透過canvas的clearRect來將滑鼠滑過的區域清除掉

  this.ctx.clearRect(x,y,width,height); //x、y為座標 width、height為清除的寬與高

這樣橡皮擦的功能就完成了,詳細的狀況可以去看我寫的code…

額外補充

canvas需要透過直接寫在HTML或是透過JS去設定寬高且無法透過CSS設定,在這樣的情況下如果要讓canvas的寬高隨螢幕resize時我們直覺會直接註冊一個resize的事件監聽,但是直接更改canvas的寬跟高會導致畫布被清空的bug…,想要解決這樣的問題我們可以透過undo、redo有用到的歷史紀錄,在螢幕resize時載入最新的一筆歷史紀錄,這樣即便在resize後畫布上的圖依然還會存在

    window.addEventListener("resize", () => {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    //先將狀態回到上一步
    var last_history = new Image();
    var window_width = window.innerWidth;
    var window_height = window.innerHeight;
    last_history.src = this.record_Arr[this.step];
    last_history.onload = () => {
      this.ctx.clearRect(0, 0, window_width, window_height);
      this.ctx.drawImage(last_history, 0, 0);
    };
  });

7F畫板 demo連結

最近對於時間管理真的非常有感觸,有很多應該做以及想做的事情像是刷題目、寫文章、進修程式、運動、看課外書、交女朋友…,這些全部都要花時間,懂得如何利用零碎的時間並增進做事效率真的很重要,希望有朝一日我也能夠成為一位專業的時間管理大師…

Photo by pixabay

補充資料

Canvas API MDN

DOM vs Canvas

Input Color MDN

Mousedown MDN

Mouse Event 小筆記

使用 DATA URI 將圖片以 Base64 編碼並內崁至網頁中,加速載入速度

//