整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          交互式觸控幼教白板如何與平板進行連接-微幼科技

          交互式觸控幼教白板如何與平板進行連接-微幼科技

          代智慧教學中離不開智能產品的輔助,電腦、平板就是其中之一。然而這兩種電子產品的顯示屏尺寸太小,并不能用于多人教學中。而55寸甚至上百寸幼教白板的出現,則可以解決多人互動教學的問題。那么,交互式觸控幼教白板如何與平板進行連接呢?

          交互式觸控幼教白板如何與平板進行連接

          其實,微幼科技幼教白板與平板的連接方式非常簡單,即平板與電子白板連接同一條網絡,然后再通過無線投屏器進行智能匹配即可。當把無線投屏器插入智能白板時,無線投屏會自動搜索設備進行匹配,當它搜索到平板時,就會自動進行連接。隨后,即可通過平板控制幼教白板,這種連接方式非常簡單而且智能。

          幼教白板即教學觸控一體機,之所以這種設備在學校如此受歡迎,是因為其本身內置有海量的教學資源,畫質清晰、音質好、十點觸控、可隨意移動安裝,集電腦、投影儀、平板、音響、幕布、白板于一體。即可以當作電腦使用,又可以用于智能教學與遠程視頻會議等。為老師提供豐富的教學資源,幫助師生體會交互作用,分析并提高教學中的認知感知能力。

          隨著觸控技術發展越來越成熟,從一點觸控到現在的十點觸控技術,互動教學、學習模式等也在不斷進行優化,從而實現一套系統多點觸控。而且這種觸控技術反應靈敏,支持一個或多個人在屏幕上進行操作,從而進行多屏互動,分屏互動等功能。

          學校本身就是為了提高教育水平和學生素質發展,而微科技的幼教白板可以為學校提供更多的教育資源優化聚集,推動教育共建共享平臺。老師在教學過程中,也可以通過遠程視頻會議的方式,與其他老師進行學習交流。從而在豐富自己的知識的同時,還可以提高教學質量。通過不同老師不同的教學模式的頭腦風暴交流,從而制定出更多適用于學生的教學模式,讓學生對學習更感興趣,進而從被動接受“填鴨式”的思維模式,轉變成主動學習的思維模式。

          雖說,55寸的教學觸控一體機的整體尺寸不大,但適用于20-30人左右的幼兒園班級綽綽有余。通過手部觸控屏幕的方式進行各種操作,更加方便快捷。同時,作為一體機的教學電子白板不需要依賴諸如投影儀、電腦和幕布等外接設備和應用程序,就可以輕松連接到所有的外接設備。讓用戶在同樣條件下進行各種互動操作,提高學生學習的興趣和效率。

          本文來源于:http://www.wekids.group/news/143-cn.html

          觸控幼教白板選購要特別注意哪些因素-微幼科技

          信各位寫文章的朋友平時肯定都有畫圖的需求,筆者平時用的是一個在線的手繪風格白板--excalidraw,使用體驗上沒的說,但是有一個問題,不能云端保存,不過好消息它是開源的,所以筆者就在想要不要基于它做一個支持云端保存的,于是三下兩除二寫了幾個接口就完成了--小白板,雖然功能完成了,但是壞消息是excalidraw是基于React的,而且代碼量很龐大,對于筆者這種常年寫Vue的人來說不是很友好,另外也無法在Vue項目上使用,于是閑著也是閑著,筆者就花了差不多一個月的業余時間來做了一個草率版的,框架無關,先來一睹為快:

          board.gif

          也可體驗在線demo:https://wanglin2.github.io/tiny_whiteboard_demo/。

          源碼倉庫在此:https://github.com/wanglin2/tiny_whiteboard。

          接下來筆者就來大致介紹一下實現的關鍵技術點。

          本文的配圖均使用筆者開發的白板進行繪制。

          簡單起見,我們以【一個矩形的一生】來看一下大致的整個流程實現。

          出生

          矩形即將出生的是一個叫做canvas的畫布世界,這個世界大致是這樣的:

          <template>
            <div class="container">
              <div class="canvasBox" ref="box"></div>
            </div>
          </template>
          
          <script setup>
              import { onMounted, ref } from "vue";
          
              const container=ref(null);
              const canvas=ref(null);
              let ctx=null;
              const initCanvas=()=> {
                  let { width, height }=container.value.getBoundingClientRect();
                  canvas.value.width=width;
                  canvas.value.height=height;
                  ctx=canvas.value.getContext("2d");
                  // 將畫布的原點由左上角移動到中心點
                  ctx.translate(width / 2, height / 2);
              };
          
              onMounted(()=> {
                  initCanvas();
              });
          </script>
          

          為什么要將畫布世界的原點移動到中心呢,其實是為了方便后續的整體放大縮小。

          矩形想要出生還缺了一樣東西,事件,否則畫布感受不到我們想要創造矩形的想法。

          // ...
          const bindEvent=()=> {
              canvas.value.addEventListener("mousedown", onMousedown);
              canvas.value.addEventListener("mousemove", onMousemove);
              canvas.value.addEventListener("mouseup", onMouseup);
          };
          const onMousedown=(e)=> {};
          const onMousemove=(e)=> {};
          const onMouseup=(e)=> {};
          
          onMounted(()=> {
              initCanvas();
              bindEvent();// ++
          });
          

          一個矩形想要在畫布世界上存在,需要明確”有多大“和”在哪里“,多大即它的width、height,哪里即它的x、y

          當我們鼠標在畫布世界按下時就決定了矩形出生的地方,所以我們需要記錄一下這個位置:

          let mousedownX=0;
          let mousedownY=0;
          let isMousedown=false;
          const onMousedown=(e)=> {
              mousedownX=e.clientX;
              mousedownY=e.clientY;
              isMousedown=true;
          };
          

          當我們的鼠標不僅按下了,還開始在畫布世界中移動的那一瞬間就會創造一個矩形了,其實我們可以創造無數個矩形,它們之間是有一些共同點的,就像我們男人一樣,好男人壞男人都是兩只眼睛一張嘴,區別只是有的人眼睛大一點,有的人比較會花言巧語而已,所以它們是存在模子的:

          // 矩形元素類
          class Rectangle {
              constructor(opt) {
                  this.x=opt.x || 0;
                  this.y=opt.y || 0;
                  this.width=opt.width || 0;
                  this.height=opt.height || 0;
              }
              render() {
                  ctx.beginPath();
                  ctx.rect(this.x, this.y, this.width, this.height);
                  ctx.stroke();
              }
          }
          

          矩形創建完成后在我們的鼠標沒有松開前都是可以修改它的初始大小的:

          // 當前激活的元素
          let activeElement=null;
          // 所有的元素
          let allElements=[];
          // 渲染所有元素
          const renderAllElements=()=> {
            allElements.forEach((element)=> {
              element.render();
            });
          }
          
          const onMousemove=(e)=> {
              if (!isMousedown) {
                  return;
              }
              // 矩形不存在就先創建一個
              if (!activeElement) {
                  activeElement=new Rectangle({
                      x: mousedownX,
                      y: mousedownY,
                  });
                  // 加入元素大家庭
                  allElements.push(activeElement);
              }
              // 更新矩形的大小
              activeElement.width=e.clientX - mousedownX;
              activeElement.height=e.clientY - mousedownY;
              // 渲染所有的元素
              renderAllElements();
          };
          

          當我們的鼠標松開后,矩形就正式出生了~

          const onMouseup=(e)=> {
              isMousedown=false;
              activeElement=null;
              mousedownX=0;
              mousedownY=0;
          };
          

          2022-04-25-15-40-29.gif

          what??和我們預想的不一樣,首先我們的鼠標是在左上角移動,但是矩形卻出生在中間位置,另外矩形大小變化的過程也顯示出來了,而我們只需要看到最后一刻的大小即可。

          其實我們鼠標是在另一個世界,這個世界的坐標原點在左上角,而前面我們把畫布世界的原點移動到中心位置了,所以它們雖然是平行世界,但是奈何坐標系不一樣,所以需要把我們鼠標的位置轉換成畫布的位置:

          const screenToCanvas=(x, y)=> {
              return {
                  x: x - canvas.value.width / 2,
                  y: y - canvas.value.height / 2
              }
          }
          

          然后在矩形渲染前先把坐標轉一轉:

          class Rectangle {
              constructor(opt) {}
          
              render() {
                  ctx.beginPath();
                  // 屏幕坐標轉成畫布坐標
                  let canvasPos=screenToCanvas(this.x, this.y);
                  ctx.rect(canvasPos.x, canvasPos.y, this.width, this.height);
                  ctx.stroke();
              }
          }
          

          另一個問題是因為在畫布世界中,你新畫一些東西時,原來畫的東西是依舊存在的,所以在每一次重新畫所有元素前都需要先把畫布清空一下:

          const clearCanvas=()=> {
              let width=canvas.value.width;
              let height=canvas.value.height;
              ctx.clearRect(-width / 2, -height / 2, width, height);
          };
          

          在每次渲染矩形前先清空畫布世界:

          const renderAllElements=()=> {
            clearCanvas();// ++
            allElements.forEach((element)=> {
              element.render();
            });
          }
          

          2022-04-25-15-41-13.gif

          恭喜矩形們成功出生~

          成長

          修理它

          小時候被爸媽修理,長大后換成被世界修理,從出生起,一切就都在變化之中,時間會磨平你的棱角,也會增加你的體重,作為畫布世界的操控者,當我們想要修理一下某個矩形時要怎么做呢?第一步,選中它,第二步,修理它。

          1.第一步,選中它

          怎么在茫茫矩形海之中選中某個矩形呢,很簡單,如果鼠標擊中了某個矩形的邊框則代表選中了它,矩形其實就是四根線段,所以只要判斷鼠標是否點擊到某根線段即可,那么問題就轉換成了,怎么判斷一個點是否和一根線段挨的很近,因為一根線很窄所以鼠標要精準點擊到是很困難的,所以我們不妨認為鼠標的點擊位置距離目標10px內都認為是擊中的。

          首先我們可以根據點到直線的計算公式來判斷一個點距離一根直線的距離:

          image-20220425095139180.png

          點到直線的距離公式為:

          image-20220425100910804.png

          // 計算點到直線的距離
          const getPointToLineDistance=(x, y, x1, y1, x2, y2)=> {
            // 直線公式y=kx+b不適用于直線垂直于x軸的情況,所以對于直線垂直于x軸的情況單獨處理
            if (x1===x2) {
              return Math.abs(x - x1);
            } else {
              let k, b;
              // y1=k * x1 + b  // 0式
              // b=y1 - k * x1  // 1式
          
              // y2=k * x2 + b    // 2式
              // y2=k * x2 + y1 - k * x1  // 1式代入2式
              // y2 - y1=k * x2 - k * x1
              // y2 - y1=k * (x2 -  x1)
              k=(y2 - y1) / (x2 -  x1) // 3式
          
              b=y1 - k * x1  // 3式代入0式
              
              return Math.abs((k * x - y + b) / Math.sqrt(1 + k * k));
            }
          };
          

          但是這樣還不夠,因為下面這種情況顯然也滿足條件但是不應該認為擊中了線段:

          image-20220425101227980.png

          因為直線是無限長的而線段不是,我們還需要再判斷一下點到線段的兩個端點的距離,這個點需要到兩個端點的距離都滿足條件才行,下圖是一個點距離線段一個端點允許的最遠的距離:

          image-20220425112504312.png

          計算兩個點的距離很簡單,公式如下:

          image.png

          這樣可以得到我們最終的函數:

          // 檢查是否點擊到了一條線段
          const checkIsAtSegment=(x, y, x1, y1, x2, y2, dis=10)=> {
            // 點到直線的距離不滿足直接返回
            if (getPointToLineDistance(x, y, x1, y1, x2, y2) > dis) {
              return false;
            }
            // 點到兩個端點的距離
            let dis1=getTowPointDistance(x, y, x1, y1);
            let dis2=getTowPointDistance(x, y, x2, y2);
            // 線段兩個端點的距離,也就是線段的長度
            let dis3=getTowPointDistance(x1, y1, x2, y2);
            // 根據勾股定理計算斜邊長度,也就是允許最遠的距離
            let max=Math.sqrt(dis * dis + dis3 * dis3);
            // 點距離兩個端點的距離都需要小于這個最遠距離
            if (dis1 <=max && dis2 <=max) {
              return true;
            }
            return false;
          };
          
          // 計算兩點之間的距離
          const getTowPointDistance=(x1, y1, x2, y2)=> {
            return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
          }
          

          然后給我們矩形的模子加一個方法:

          class Rectangle {
              // 檢測是否被擊中
              isHit(x0, y0) {
                  let { x, y, width, height }=this;
                  // 矩形四條邊的線段
                  let segments=[
                      [x, y, x + width, y],
                      [x + width, y, x + width, y + height],
                      [x + width, y + height, x, y + height],
                      [x, y + height, x, y],
                  ];
                  for (let i=0; i < segments.length; i++) {
                      let segment=segments[i];
                      if (
                          checkIsAtSegment(x0, y0, segment[0], segment[1], segment[2], segment[3])
                      ) {
                          return true;
                      }
                  }
                  return false;
              }
          }
          

          現在我們可以來修改一下鼠標按下的函數,判斷我們是否擊中了一個矩形:

          const onMousedown=(e)=> {
            // ...
            if (currentType.value==='selection') {
              // 選擇模式下進行元素激活檢測
              checkIsHitElement(mousedownX, mousedownY);
            }
          };
          
          // 檢測是否擊中了某個元素
          const checkIsHitElement=(x, y)=> {
            let hitElement=null;
            // 從后往前遍歷元素,即默認認為新的元素在更上層
            for (let i=allElements.length - 1; i >=0; i--) {
              if (allElements[i].isHit(x, y)) {
                hitElement=allElements[i];
                break;
              }
            }
            if (hitElement) {
              alert("擊中了矩形");
            }
          };
          

          2022-04-25-15-43-04.gif

          可以看到雖然我們成功選中了矩形,但是卻意外的又創造了一個新矩形,要避免這種情況我們可以新增一個變量來區分一下當前是創造矩形還是選擇矩形,在正確的時候做正確的事:

          <template>
            <div class="container" ref="container">
              <canvas ref="canvas"></canvas>
              <div class="toolbar">
                <el-radio-group v-model="currentType">
                  <el-radio-button label="selection">選擇</el-radio-button>
                  <el-radio-button label="rectangle">矩形</el-radio-button>
                </el-radio-group>
              </div>
            </div>
          </template>
          
          <script setup>
          // ...
          // 當前操作模式
          const currentType=ref('selection');
          </script>
          

          選擇模式下可以選擇矩形,但是不能創造新矩形,修改一下鼠標移動的方法:

          const onMousemove=(e)=> {
            if (!isMousedown || currentType.value==='selection') {
              return;
            }
          }
          

          2022-04-25-15-44-43.gif

          最后,選中一個矩形時為了能突出它被選中以及為了緊接著能修理它,我們給它外圍畫個虛線框,并再添加上一些操作手柄,先給矩形模子增加一個屬性,代表它被激活了:

          class Rectangle {
            constructor(opt) {
              // ...
              this.isActive=false;
            }
          }
          

          然后再給它添加一個方法,當激活時渲染激活態圖形:

          class Rectangle {
            render() {
              let canvasPos=screenToCanvas(this.x, this.y);
              drawRect(canvasPos.x, canvasPos.y, this.width, this.height);
              this.renderActiveState();// ++
            }
          
            // 當激活時渲染激活態
            renderActiveState() {
              if (!this.isActive) {
                return;
              }
              let canvasPos=screenToCanvas(this.x, this.y);
              // 為了不和矩形重疊,虛線框比矩形大一圈,增加5px的內邊距
              let x=canvasPos.x - 5;
              let y=canvasPos.y - 5;
              let width=this.width + 10;
              let height=this.height + 10;
              // 主體的虛線框
              ctx.save();
              ctx.setLineDash([5]);
              drawRect(x, y, width, height);
              ctx.restore();
              // 左上角的操作手柄
              drawRect(x - 10, y - 10, 10, 10);
              // 右上角的操作手柄
              drawRect(x + width, y - 10, 10, 10);
              // 右下角的操作手柄
              drawRect(x + width, y + height, 10, 10);
              // 左下角的操作手柄
              drawRect(x - 10, y + height, 10, 10);
              // 旋轉操作手柄
              drawCircle(x + width / 2, y - 10, 10);
            }
          }
          
          // 提取出公共的繪制矩形和圓的方法
          // 繪制矩形
          const drawRect=(x, y, width, height)=> {
            ctx.beginPath();
            ctx.rect(x, y, width, height);
            ctx.stroke();
          };
          // 繪制圓形
          const drawCircle=(x, y, r)=> {
            ctx.beginPath();
            ctx.arc(x, y, r, 0, 2 * Math.PI);
            ctx.stroke();
          };
          

          最后修改一下檢測是否擊中了元素的方法:

          const checkIsHitElement=(x, y)=> {
            // ...
            // 如果當前已經有激活元素則先將它取消激活
            if (activeElement) {
              activeElement.isActive=false;
            }
            // 更新當前激活元素
            activeElement=hitElement;
            if (hitElement) {
              // 如果當前擊中了元素,則將它的狀態修改為激活狀態
              hitElement.isActive=true;
            }
            // 重新渲染所有元素
            renderAllElements();
          };
          

          2022-04-25-15-36-09.gif

          可以看到激活新的矩形時并沒有將之前的激活元素取消掉,原因出在我們的鼠標松開的處理函數,因為我們之前的處理是鼠標松開時就把activeElement復位成了null,修改一下:

          const onMouseup=(e)=> {
            isMousedown=false;
            // 選擇模式下就不需要復位了
            if (currentType.value !=='selection') {
              activeElement=null;
            }
            mousedownX=0;
            mousedownY=0;
          };
          

          2022-04-25-15-37-20.gif

          2.第二步,修理它

          終于到了萬眾矚目的修理環節,不過別急,在修理之前我們還要做一件事,那就是得要知道我們鼠標具體在哪個操作手柄上,當我們激活一個矩形,它會顯示激活態,然后再當我們按住了激活態的某個部位進行拖動時進行具體的修理操作,比如按住了中間的大虛線框里面則進行移動操作,按住了旋轉手柄則進行矩形的旋轉操作,按住了其他的四個角的操作手柄之一則進行矩形的大小調整操作。

          具體的檢測來說,中間的虛線框及四個角的調整手柄,都是判斷一個點是否在矩形內,這個很簡單:

          // 判斷一個坐標是否在一個矩形內
          const checkPointIsInRectangle=(x, y, rx, ry, rw, rh)=> {
            return x >=rx && x <=rx + rw && y >=ry && y <=ry + rh;
          };
          

          旋轉按鈕是個圓,那么我們只要判斷一個點到其圓心的距離,小于半徑則代表在圓內,那么我們可以給矩形模子加上激活狀態各個區域的檢測方法:

          class Rectangle {
            // 檢測是否擊中了激活狀態的某個區域
            isHitActiveArea(x0, y0) {
              let x=this.x - 5;
              let y=this.y - 5;
              let width=this.width + 10;
              let height=this.height + 10;
              if (checkPointIsInRectangle(x0, y0, x, y, width, height)) {
                // 在中間的虛線框
                return "body";
              } else if (getTowPointDistance(x0, y0, x + width / 2, y - 10) <=10) {
                // 在旋轉手柄
                return "rotate";
              } else if (checkPointIsInRectangle(x0, y0, x + width, y + height, 10, 10)) {
                // 在右下角操作手柄
                return "bottomRight";
              }
            }
          }
          

          簡單起見,四個角的操作手柄我們只演示右下角的一個,其他三個都是一樣的,各位可以自行完善。

          接下來又需要修改鼠標按下的方法,如果當前是選擇模式,且已經有激活的矩形時,那么我們就判斷是否按住了這個激活矩形的某個激活區域,如果確實按在了某個激活區域內,那么我們就設置兩個標志位,記錄當前是否處于矩形的調整狀態中以及具體處在哪個區域,否則就進行原來的更新當前激活的矩形邏輯:

          // 當前是否正在調整元素
          let isAdjustmentElement=false;
          // 當前按住了激活元素激活態的哪個區域
          let hitActiveElementArea="";
          
          const onMousedown=(e)=> {
            mousedownX=e.clientX;
            mousedownY=e.clientY;
            isMousedown=true;
            if (currentType.value==="selection") {
              // 選擇模式下進行元素激活檢測
              if (activeElement) {
                // 當前存在激活元素則判斷是否按住了激活狀態的某個區域
                let hitActiveArea=activeElement.isHitActiveArea(mousedownX, mousedownY);
                if (hitActiveArea) {
                  // 按住了按住了激活狀態的某個區域
                  isAdjustmentElement=true;
                  hitActiveElementArea=hitArea;
                  alert(hitActiveArea);
                } else {
                  // 否則進行激活元素的更新操作
                  checkIsHitElement(mousedownX, mousedownY);
                }
              } else {
                checkIsHitElement(mousedownX, mousedownY);
              }
            }
          };
          

          2022-04-25-15-34-01.gif

          當鼠標按住了矩形激活狀態的某個區域并且鼠標開始移動時即代表進行矩形修理操作,先來看按住了虛線框時的矩形移動操作。

          移動矩形

          移動矩形很簡單,修改它的x、y即可,首先計算鼠標當前位置和鼠標按下時的位置之差,然后把這個差值加到鼠標按下時那一瞬間的矩形的x、y上作為矩形新的坐標,那么這之前又得來修改一下咱們的矩形模子:

          class Rectangle {
            constructor(opt) {
              this.x=opt.x || 0;
              this.y=opt.y || 0;
              // 記錄矩形的初始位置
              this.startX=0;// ++
              this.startY=0;// ++
              // ...
            }
              
            // 保存矩形某一刻的狀態
            save() {
              this.startX=this.x;
              this.startY=this.y;
            }
          
            // 移動矩形
            moveBy(ox, oy) {
              this.x=this.startX + ox;
              this.y=this.startY + oy;
            }
          }
          

          啥時候保存矩形的狀態呢,當然是鼠標按住了矩形激活狀態的某個區域時:

          const onMousedown=(e)=> {
              // ...
              if (currentType.value==="selection") {
                  if (activeElement) {
                      if (hitActiveArea) {
                          // 按住了按住了激活狀態的某個區域
                          isAdjustmentElement=true;
                          hitActiveElementArea=hitArea;
                          activeElement.save();// ++
                      }
                  }
                  // ...
              }
          }
          

          然后當鼠標移動時就可以進行進行的移動操作了:

          const onMousemove=(e)=> {
            if (!isMousedown) {
              return;
            }
            if (currentType.value==="selection") {
              if (isAdjustmentElement) {
                // 調整元素中
                let ox=e.clientX - mousedownX;
                let oy=e.clientY - mousedownY;
                if (hitActiveElementArea==="body") {
                  // 進行移動操作
                  activeElement.moveBy(ox, oy);
                }
                renderAllElements();
              }
              return;
            }
            // ...
          }
          

          不要忘記當鼠標松開時恢復標志位:

          const onMouseup=(e)=> {
            // ...
            if (isAdjustmentElement) {
              isAdjustmentElement=false;
              hitActiveElementArea="";
            }
          };
          

          2022-04-25-17-11-54.gif

          旋轉矩形

          先來修改一下矩形的模子,給它加上旋轉的角度屬性:

          class Rectangle {
              constructor(opt) {
                  // ...
                  // 旋轉角度
                  this.rotate=opt.rotate || 0;
                  // 記錄矩形的初始角度
                  this.startRotate=0;
              }
          }
          

          然后修改它的渲染方法:

          class Rectangle {
              render() {
                  ctx.save();// ++
                  let canvasPos=screenToCanvas(this.x, this.y);
                  ctx.rotate(degToRad(this.rotate));// ++
                  drawRect(canvasPos.x, canvasPos.y, this.width, this.height);
                  this.renderActiveState();
                  ctx.restore();// ++
              }
          }
          

          畫布的rotate方法接收弧度為單位的值,我們保存角度值,所以需要把角度轉成弧度,角度和弧度的互轉公式如下:

          因為360度=2PI
          即180度=PI
          所以:
          
          1弧度=(180/π)°角度
          1角度=π/180弧度
          
          // 弧度轉角度
          const radToDeg=(rad)=> {
            return rad * (180 / Math.PI);
          };
          
          // 角度轉弧度
          const degToRad=(deg)=> {
            return deg * (Math.PI / 180);
          };
          

          然后和前面修改矩形的坐標套路一樣,旋轉時先保存初始角度,然后旋轉時更新角度:

          class Rectangle {
              // 保存矩形此刻的狀態
              save() {
                  // ...
                  this.startRotate=this.rotate;
              }
          
              // 旋轉矩形
              rotateBy(or) {
                  this.rotate=this.startRotate + or;
              }
          }
          

          接下來的問題就是如何計算鼠標移動的角度了,即鼠標按下的位置到鼠標當前移動到的位置經過的角度,兩個點本身并不存在啥角度,只有相對一個中心點會形成角度:

          image-20220425181312806.png

          這個中心點其實就是矩形的中心點,上圖夾角的計算可以根據這兩個點與中心點組成的線段和水平x軸形成的角度之差進行計算:

          image-20220425181845910.png

          這兩個夾角的正切值等于它們的對邊除以鄰邊,對邊和鄰邊我們都可以計算出來,所以使用反正切函數即可計算出這兩個角,最后再計算一下差值即可:

          // 計算兩個坐標以同一個中心點構成的角度
          const getTowPointRotate=(cx, cy, tx, ty, fx, fy)=> {
            // 計算出來的是弧度值,所以需要轉成角度
            return radToDeg(Math.atan2(fy - cy, fx - cx) - Math.atan2(ty - cy, tx - cx));
          }
          

          有了這個方法,接下來我們修改鼠標移動的函數:

          const onMousemove=(e)=> {
            if (!isMousedown) {
              return;
            }
            if (currentType.value==="selection") {
              if (isAdjustmentElement) {
                if (hitActiveElementArea==="body") {
                  // 進行移動操作
                } else if (hitActiveElementArea==='rotate') {
                  // 進行旋轉操作
                  // 矩形的中心點
                  let center=getRectangleCenter(activeElement);
                  // 獲取鼠標移動的角度
                  let or=getTowPointRotate(center.x, center.y, mousedownX, mousedownY, e.clientX, e.clientY);
                  activeElement.rotateBy(or);
                }
                renderAllElements();
              }
              return;
            }
            // ...
          }
          
          // 計算矩形的中心點
          const getRectangleCenter=({x, y, width, height})=> {
            return {
              x: x + width / 2,
              y: y + height / 2,
            };
          }
          

          2022-04-25-18-40-49.gif

          可以看到確實旋轉了,但是顯然不是我們要的旋轉,我們要的是矩形以自身中心進行旋轉,動圖里明顯不是,這其實是因為canvas畫布的rotate方法是以畫布原點為中心進行旋轉的,所以繪制矩形時需要再移動一下畫布原點,移動到自身的中心,然后再進行繪制,這樣旋轉就相當于以自身的中心進行旋轉了,不過需要注意的是,原點變了,矩形本身和激活狀態的相關圖形的繪制坐標均需要修改一下:

          class Rectangle {
              render() {
                  ctx.save();
                  let canvasPos=screenToCanvas(this.x, this.y);
                  // 將畫布原點移動到自身的中心
                  let halfWidth=this.width / 2
                  let halfHeight=this.height / 2
                  ctx.translate(canvasPos.x + halfWidth, canvasPos.y + halfHeight);
                  // 旋轉
                  ctx.rotate(degToRad(this.rotate));
                  // 原點變成自身中心,那么自身的坐標x,y也需要轉換一下,即:canvasPos.x - (canvasPos.x + halfWidth),其實就變成了(-halfWidth, -halfHeight)
                  drawRect(-halfWidth, -halfHeight, this.width, this.height);
                  this.renderActiveState();
                  ctx.restore();
              }
          
              renderActiveState() {
                  if (!this.isActive) {
                      return;
                  }
                  let halfWidth=this.width / 2     // ++
                  let halfHeight=this.height / 2   // ++
                  let x=-halfWidth - 5;            // this.x -> -halfWidth
                  let y=-halfHeight - 5;     // this.y -> -halfHeight
                  let width=this.width + 10;
                  let height=this.height + 10;
                  // ...
              }
          }
          

          2022-04-25-19-08-00.gif

          旋轉后的問題

          2022-04-25-19-10-40.gif

          矩形旋轉后會發現一個問題,我們明明鼠標點擊在進行的邊框上,但是卻無法激活它,矩形想擺脫我們的控制?它想太多,原因其實很簡單:

          image-20220425192046034.png

          虛線是矩形沒有旋轉時的位置,我們點擊在了旋轉后的邊框上,但是我們的點擊檢測是以矩形沒有旋轉時進行的,因為矩形雖然旋轉了,但是本質上它的x、y坐標并沒有變,知道了原因解決就很簡單了,我們不妨把鼠標指針的坐標以矩形中心為原點反向旋轉矩形旋轉的角度:

          image-20220425192752165.png

          好了,問題又轉化成了如何求一個坐標旋轉指定角度后的坐標:

          image-20220425200034610.png

          如上圖所示,計算p1O為中心逆時針旋轉黑色角度后的p2坐標,首先根據p1的坐標計算綠色角度的反正切值,然后加上已知的旋轉角度得到紅色的角度,無論怎么旋轉,這個點距離中心的點的距離都是不變的,所以我們可以計算出p1到中心點O的距離,也就是P2到點O的距離,斜邊的長度知道了, 紅色的角度也知道了,那么只要根據正余弦定理即可計算出對邊和鄰邊的長度,自然p2的坐標就知道了:

          // 獲取坐標經指定中心點旋轉指定角度的坐標
          const getRotatedPoint=(x, y, cx, cy, rotate)=> {
            let deg=radToDeg(Math.atan2(y - cy, x - cx));
            let del=deg + rotate;
            let dis=getTowPointDistance(x, y, cx, cy);
            return {
              x: Math.cos(degToRad(del)) * dis + cx,
              y: Math.sin(degToRad(del)) * dis + cy,
            };
          };
          

          最后,修改一下矩形的點擊檢測方法:

          class Rectangle {
              // 檢測是否被擊中
              isHit(x0, y0) {
                  // 反向旋轉矩形的角度
                  let center=getRectangleCenter(this);
                  let rotatePoint=getRotatedPoint(x0, y0, center.x, center.y, -this.rotate);
                  x0=rotatePoint.x;
                  y0=rotatePoint.y;
                  // ...
              }
          
              // 檢測是否擊中了激活狀態的某個區域
              isHitActiveArea(x0, y0) {
                  // 反向旋轉矩形的角度
                  let center=getRectangleCenter(this);
                  let rotatePoint=getRotatedPoint(x0, y0, center.x, center.y, -this.rotate);
                  x0=rotatePoint.x;
                  y0=rotatePoint.y;
                  // ...
              }
          }
          

          2022-04-25-20-19-44.gif

          伸縮矩形

          最后一種修理矩形的方式就是伸縮矩形,即調整矩形的大小,如下圖所示:

          image-20220426094039264.png

          虛線為伸縮前的矩形,實線為按住矩形右下角伸縮手柄拖動后的新矩形,矩形是由x、y、width、height四個屬性構成的,所以計算伸縮后的矩形,其實也就是計算出新矩形的x、y、width、height,計算步驟如下(以下思路來自于https://github.com/shenhudong/snapping-demo/wiki/corner-handle。):

          1.鼠標按下伸縮手柄后,計算出矩形這個角的對角點坐標diagonalPoint

          image-20220426095731343.png

          2.根據鼠標當前移動到的位置,再結合對角點diagonalPoint可以計算出新矩形的中心點newCenter

          image-20220426100228212.png

          3.新的中心點知道了,那么我們就可以把鼠標當前的坐標以新中心點反向旋轉元素的角度,即可得到新矩形未旋轉時的右下角坐標rp

          image-20220426100551601.png

          4.中心點坐標有了,右下角坐標也有了,那么計算新矩形的x、y、wdith、height都很簡單了:

          let width=(rp.x - newCenter.x) * 2
          let height=(rp.y- newCenter.y * 2
          let x=rp.x - width
          let y=rp.y - height
          

          接下來看代碼實現,首先修改一下矩形的模子,新增幾個屬性:

          class Rectangle {
              constructor(opt) {
                  // ...
                  // 對角點坐標
                  this.diagonalPoint={
                      x: 0,
                      y: 0
                  }
                  // 鼠標按下位置和元素的角坐標的差值,因為我們是按住了拖拽手柄,這個按下的位置是和元素的角坐標存在一定距離的,所以為了不發生突變,需要記錄一下這個差值
                  this.mousedownPosAndElementPosOffset={
                      x: 0,
                      y: 0
                  }
              }
          }
          

          然后修改一下矩形保存狀態的save方法:

          class Rectangle {
            // 保存矩形此刻的狀態
            save(clientX, clientY, hitArea) {// 增加幾個入參
              // ...
              if (hitArea==="bottomRight") {
                // 矩形的中心點坐標
                let centerPos=getRectangleCenter(this);
                // 矩形右下角的坐標
                let pos={
                  x: this.x + this.width,
                  y: this.y + this.height,
                };
                // 如果元素旋轉了,那么右下角坐標也要相應的旋轉
                let rotatedPos=getRotatedPoint(pos.x, pos.y, centerPos.x, centerPos.y, this.rotate);
                // 計算對角點的坐標
                this.diagonalPoint.x=2 * centerPos.x - rotatedPos.x;
                this.diagonalPoint.y=2 * centerPos.y - rotatedPos.y;
                // 計算鼠標按下位置和元素的左上角坐標差值
                this.mousedownPosAndElementPosOffset.x=clientX - rotatedPos.x;
                this.mousedownPosAndElementPosOffset.y=clientY - rotatedPos.y;
              }
            }
          }
          

          save方法增加了幾個傳參,所以也要相應修改一下鼠標按下的方法,在調用save的時候傳入鼠標當前的位置和按住了激活態的哪個區域。

          接下來我們再給矩形的模子增加一個伸縮的方法:

          class Rectangle {
            // 伸縮
            stretch(clientX, clientY, hitArea) {
              // 鼠標當前的坐標減去偏移量得到矩形這個角的坐標
              let actClientX=clientX - this.mousedownPosAndElementPosOffset.x;
              let actClientY=clientY - this.mousedownPosAndElementPosOffset.y;
              // 新的中心點
              let newCenter={
                x: (actClientX + this.diagonalPoint.x) / 2,
                y: (actClientY + this.diagonalPoint.y) / 2,
              };
              // 獲取新的角坐標經新的中心點反向旋轉元素的角度后的坐標,得到矩形未旋轉前的這個角坐標
              let rp=getRotatedPoint(
                actClientX,
                actClientY,
                newCenter.x,
                newCenter.y,
                -this.rotate
              );
              if (hitArea==="bottomRight") {
                // 計算新的大小
                this.width=(rp.x - newCenter.x) * 2;
                this.height=(rp.y - newCenter.y) * 2;
                // 計算新的位置
                this.x=rp.x - this.width;
                this.y=rp.y - this.height;
              }
            }
          }
          

          最后,讓我們在鼠標移動函數里調用這個方法:

          const onMousemove=(e)=> {
            if (!isMousedown) {
              return;
            }
            if (currentType.value==="selection") {
              if (isAdjustmentElement) {
                if (hitActiveElementArea==="body") {
                  // 進行移動操作
                } else if (hitActiveElementArea==='rotate') {
                  // 進行旋轉操作
                } else if (hitActiveElementArea==='bottomRight') {
                  // 進行伸縮操作
                  activeElement.stretch(e.clientX, e.clientY, hitActiveElementArea);
                }
                renderAllElements();
              }
              return;
            }
            // ...
          }
          

          2022-04-26-15-22-47.gif

          世界太小了

          有一天我們的小矩形說,世界這么大,它想去看看,確實,屏幕就這么大,矩形肯定早就待膩了,作為萬能的畫布操控者,讓我們來滿足它的要求。

          我們新增兩個狀態變量:scrollXscrollY,記錄畫布水平和垂直方向的滾動偏移量,以垂直方向的偏移量來介紹,當鼠標滾動時,增加或減少scrollY,但是這個滾動值我們不直接應用到畫布上,而是在繪制矩形的時候加上去,比如矩形用來的y100,我們向上滾動了100px,那么實際矩形繪制的時候的y=100-100=0,這樣就達到了矩形也跟著滾動的效果。

          // 當前滾動值
          let scrollY=0;
          
          // 監聽事件
          const bindEvent=()=> {
            // ...
            canvas.value.addEventListener("mousewheel", onMousewheel);
          };
          
          // 鼠標移動事件
          const onMousewheel=(e)=> {
            if (e.wheelDelta < 0) {
              // 向下滾動
              scrollY +=50;
            } else {
              // 向上滾動
              scrollY -=50;
            }
            // 重新渲染所有元素
            renderAllElements();
          };
          

          然后我們再繪制矩形時加上這個滾動偏移量:

          class Rectangle {
              render() {
                  ctx.save();
                  let _x=this.x;
                  let _y=this.y - scrollY;
                  let canvasPos=screenToCanvas(_x, _y);
                  // ...
              }
          }
          

          2022-04-26-16-06-53.gif

          是不是很簡單,但是問題又來了,因為滾動后會發現我們又無法激活矩形了,而且繪制矩形也出問題了:

          2022-04-26-16-11-26.gif

          原因和矩形旋轉一樣,滾動只是最終繪制的時候加上了滾動值,但是矩形的x、y仍舊沒有變化,因為繪制時是減去了scrollY,那么我們獲取到的鼠標的clientY不妨加上scrollY,這樣剛好抵消了,修改一下鼠標按下和鼠標移動的函數:

          const onMousedown=(e)=> {
              let _clientX=e.clientX;
              let _clientY=e.clientY + scrollY;
              mousedownX=_clientX;
              mousedownY=_clientY;
              // ...
          }
          
          const onMousemove=(e)=> {
              if (!isMousedown) {
                  return;
              }
              let _clientX=e.clientX;
              let _clientY=e.clientY + scrollY;
              if (currentType.value==="selection") {
                  if (isAdjustmentElement) {
                      let ox=_clientX - mousedownX;
                      let oy=_clientY - mousedownY;
                      if (hitActiveElementArea==="body") {
                          // 進行移動操作
                      } else if (hitActiveElementArea==="rotate") {
                          // ...
              let or=getTowPointRotate(
                            center.x,
                            center.y,
                            mousedownX,
                            mousedownY,
                            _clientX,
                            _clientY
                          );
                          // ...
                      }
                  }
              }
              // ...
              // 更新矩形的大小
             activeElement.width=_clientX - mousedownX;
             activeElement.height=_clientY - mousedownY;
              // ...
          }
          

          反正把之前所有使用e.clientY的地方都修改成加上scrollY后的值。

          2022-04-26-16-18-21.gif

          距離產生美

          有時候矩形太小了我們想近距離看看,有時候太大了我們又想離遠一點,怎么辦呢,很簡單,加個放大縮小的功能!

          新增一個變量scale

          // 當前縮放值
          let scale=1;
          

          然后當我們繪制元素前縮放一下畫布即可:

          // 渲染所有元素
          const renderAllElements=()=> {
            clearCanvas();
            ctx.save();// ++
            // 整體縮放
            ctx.scale(scale, scale);// ++
            allElements.forEach((element)=> {
              element.render();
            });
            ctx.restore();// ++
          };
          

          添加兩個按鈕,以及兩個放大縮小的函數:

          // 放大
          const zoomIn=()=> {
            scale +=0.1;
            renderAllElements();
          };
          
          // 縮小
          const zoomOut=()=> {
            scale -=0.1;
            renderAllElements();
          };
          

          2022-04-26-16-44-38.gif

          問題又又又來了朋友們,我們又無法激活矩形以及創造新矩形又出現偏移了:

          2022-04-26-16-50-02.gif

          還是老掉牙的原因,無論怎么滾動縮放旋轉,矩形的x、y本質都是不變的,沒辦法,轉換吧:

          image-20220426170111431.png

          同樣是修改鼠標的clientX、clientY,先把鼠標坐標轉成畫布坐標,然后縮小畫布的縮放值,最后再轉成屏幕坐標即可:

          const onMousedown=(e)=> {
            // 處理縮放
            let canvasClient=screenToCanvas(e.clientX, e.clientY);// 屏幕坐標轉成畫布坐標
            let _clientX=canvasClient.x / scale;// 縮小畫布的縮放值
            let _clientY=canvasClient.y / scale;
            let screenClient=canvasToScreen(_clientX, _clientY)// 畫布坐標轉回屏幕坐標
            // 處理滾動
            _clientX=screenClient.x;
            _clientY=screenClient.y + scrollY;
            mousedownX=_clientX;
            mousedownY=_clientY;
            // ...
          }
          // onMousemove方法也是同樣處理
          

          2022-04-26-17-10-04.gif

          能不能整齊一點

          如果我們想讓兩個矩形對齊,靠手來操作是很難的,解決方法一般有兩個,一是增加吸附的功能,二是通過網格,吸附功能是需要一定計算量的,本來咱們就不富裕的性能就更加雪上加霜了,所以咱們選擇使用網格。

          先來增加個畫網格的方法:

          // 渲染網格
          const renderGrid=()=> {
            ctx.save();
            ctx.strokeStyle="#dfe0e1";
            let width=canvas.value.width;
            let height=canvas.value.height;
            // 水平線,從上往下畫
            for (let i=-height / 2; i < height / 2; i +=20) {
              drawHorizontalLine(i);
            }
            // 垂直線,從左往右畫
            for (let i=-width / 2; i < width / 2; i +=20) {
              drawVerticalLine(i);
            }
            ctx.restore();
          };
          // 繪制網格水平線
          const drawHorizontalLine=(i)=> {
            let width=canvas.value.width;
            // 不要忘了繪制網格也需要減去滾動值
            let _i=i - scrollY;
            ctx.beginPath();
            ctx.moveTo(-width / 2, _i);
            ctx.lineTo(width / 2, _i);
            ctx.stroke();
          };
          // 繪制網格垂直線
          const drawVerticalLine=(i)=> {
            let height=canvas.value.height;
            ctx.beginPath();
            ctx.moveTo(i, -height / 2);
            ctx.lineTo(i, height / 2);
            ctx.stroke();
          };
          

          代碼看著很多,但是邏輯很簡單,就是從上往下掃描和從左往右掃描,然后在繪制元素前先繪制一些網格:

          const renderAllElements=()=> {
            clearCanvas();
            ctx.save();
            ctx.scale(scale, scale);
            renderGrid();// ++
            allElements.forEach((element)=> {
              element.render();
            });
            ctx.restore();
          };
          

          進入頁面就先調用一下這個方法即可顯示網格:

          onMounted(()=> {
            initCanvas();
            bindEvent();
            renderAllElements();// ++
          });
          

          image-20220426184526124.png

          到這里我們雖然繪制了網格,但是實際上沒啥用,它并不能限制我們,我們需要繪制網格的時候讓矩形貼著網格的邊,這樣繪制多個矩形的時候就能輕松的實現對齊了。

          這個怎么做呢,很簡單,因為網格也相當于是從左上角開始繪制的,所以我們獲取到鼠標的clientX、clientY后,對網格的大小進行取余,然后再減去這個余數,即可得到最近可以吸附到的網格坐標:

          image-20220426185905438.png

          如上圖所示,網格大小為20,鼠標坐標是(65,65)x、y都取余計算65%20=5,然后均減去5得到吸附到的坐標(60,60)

          接下來修改onMousedownonMousemove函數,需要注意的是這個吸附僅用于繪制圖形,點擊檢測我們還是要使用未吸附的坐標:

          const onMousedown=(e)=> {
              // 處理縮放
              // ...
              // 處理滾動
              _clientX=screenClient.x;
              _clientY=screenClient.y + scrollY;
              // 吸附到網格
              let gridClientX=_clientX - _clientX % 20;
              let gridClientY=_clientY - _clientY % 20;
              mousedownX=gridClientX;// 改用吸附到網格的坐標
              mousedownY=gridClientY;
              // ...
              // 后面進行元素檢測的坐標我們還是使用_clientX、_clientY,保存矩形當前狀態的坐標需要換成使用gridClientX、gridClientY
              activeElement.save(gridClientX, gridClientY, hitArea);
              // ...
          }
          
          const onMousemove=(e)=> {
              // 處理縮放
              // ...
              // 處理滾動
              _clientX=screenClient.x;
              _clientY=screenClient.y + scrollY;
              // 吸附到網格
              let gridClientX=_clientX - _clientX % 20;
              let gridClientY=_clientY - _clientY % 20;
              // 后面所有的坐標都由_clientX、_clientY改成使用gridClientX、gridClientY
          }
          

          2022-04-26-19-40-51.gif

          當然,上述的代碼還是有不足的,當我們滾動或縮小后,網格就沒有鋪滿頁面了:

          2022-04-26-20-09-36.gif

          解決起來也不難,比如上圖,縮小以后,水平線沒有延伸到兩端,因為縮小后相當于寬度變小了,那我們只要繪制水平線時讓寬度變大即可,那么可以除以縮放值:

          const drawHorizontalLine=(i)=> {
            let width=canvas.value.width;
            let _i=i + scrollY;
            ctx.beginPath();
            ctx.moveTo(-width / scale / 2, _i);// ++
            ctx.lineTo(width / scale / 2, _i);// ++
            ctx.stroke();
          };
          

          垂直線也是一樣。

          而當發生滾動后,比如向下滾動,那么上方的水平線沒了,那我們只要補畫一下上方的水平線,水平線我們是從-height/2開始向下畫到height/2,那么我們就從-height/2開始再向上補畫:

          const renderGrid=()=> {
              // ...
              // 水平線
              for (let i=-height / 2; i < height / 2; i +=20) {
                  drawHorizontalLine(i);
              }
              // 向下滾時繪制上方超出部分的水平線
              for (
                  let i=-height / 2 - 20;
                  i > -height / 2 + scrollY;
                  i -=20
              ) {
                  drawHorizontalLine(i);
              }
              // ...
          }
          

          限于篇幅就不再展開,各位可以閱讀源碼或自行完善。

          照個相吧

          如果我們想記錄某一時刻矩形的美要怎么做呢,簡單,導出成圖片就可以了。

          導出圖片不能簡單的直接把畫布導出就行了,因為當我們滾動或放大后,矩形也許都在畫布外了,或者只有一個小矩形,而我們把整個畫布都導出了也屬實沒有必要,我們可以先計算出所有矩形的公共外包圍框,然后另外創建一個這么大的畫布,把所有元素在這個畫布里也繪制一份,然后再導出這個畫布即可。

          計算所有元素的外包圍框可以先計算出每一個矩形的四個角的坐標,注意是要旋轉之后的,然后再循環所有元素進行比較,計算出minx、maxx、miny、maxy即可。

          // 獲取多個元素的最外層包圍框信息
          const getMultiElementRectInfo=(elementList=[])=> {
            if (elementList.length <=0) {
              return {
                minx: 0,
                maxx: 0,
                miny: 0,
                maxy: 0,
              };
            }
            let minx=Infinity;
            let maxx=-Infinity;
            let miny=Infinity;
            let maxy=-Infinity;
            elementList.forEach((element)=> {
              let pointList=getElementCorners(element);
              pointList.forEach(({ x, y })=> {
                if (x < minx) {
                  minx=x;
                }
                if (x > maxx) {
                  maxx=x;
                }
                if (y < miny) {
                  miny=y;
                }
                if (y > maxy) {
                  maxy=y;
                }
              });
            });
            return {
              minx,
              maxx,
              miny,
              maxy,
            };
          }
          // 獲取元素的四個角的坐標,應用了旋轉之后的
          const getElementCorners=(element)=> {
            // 左上角
            let topLeft=getElementRotatedCornerPoint(element, "topLeft")
            // 右上角
            let topRight=getElementRotatedCornerPoint(element, "topRight");
            // 左下角
            let bottomLeft=getElementRotatedCornerPoint(element, "bottomLeft");
            // 右下角
            let bottomRight=getElementRotatedCornerPoint(element, "bottomRight");
            return [topLeft, topRight, bottomLeft, bottomRight];
          }
          // 獲取元素旋轉后的四個角坐標
          const getElementRotatedCornerPoint=(element, dir)=> {
            // 元素中心點
            let center=getRectangleCenter(element);
            // 元素的某個角坐標
            let dirPos=getElementCornerPoint(element, dir);
            // 旋轉元素的角度
            return getRotatedPoint(
              dirPos.x,
              dirPos.y,
              center.x,
              center.y,
              element.rotate
            );
          };
          // 獲取元素的四個角坐標
          const getElementCornerPoint=(element, dir)=> {
            let { x, y, width, height }=element;
            switch (dir) {
              case "topLeft":
                return {
                  x,
                  y,
                };
              case "topRight":
                return {
                  x: x + width,
                  y,
                };
              case "bottomRight":
                return {
                  x: x + width,
                  y: y + height,
                };
              case "bottomLeft":
                return {
                  x,
                  y: y + height,
                };
              default:
                break;
            }
          };
          

          代碼很多,但是邏輯很簡單,計算出了所有元素的外包圍框信息,接下來就可以創建一個新畫布以及把元素繪制上去:

          // 導出為圖片
          const exportImg=()=> {
            // 計算所有元素的外包圍框信息
            let { minx, maxx, miny, maxy }=getMultiElementRectInfo(allElements);
            let width=maxx - minx;
            let height=maxy - miny;
            // 替換之前的canvas
            canvas.value=document.createElement("canvas");
            canvas.value.style.cssText=`
              position: absolute;
              left: 0;
              top: 0;
              border: 1px solid red;
              background-color: #fff;
            `;
            canvas.value.width=width;
            canvas.value.height=height;
            document.body.appendChild(canvas.value);
            // 替換之前的繪圖上下文
            ctx=canvas.value.getContext("2d");
            // 畫布原點移動到畫布中心
            ctx.translate(canvas.value.width / 2, canvas.value.height / 2);
            // 將滾動值恢復成0,因為在新畫布上并不涉及到滾動,所有元素距離有多遠我們就會創建一個有多大的畫布
            scrollY=0;
            // 渲染所有元素
            allElements.forEach((element)=> {
              // 這里為什么要減去minx、miny呢,因為比如最左上角矩形的坐標為(100,100),所以min、miny計算出來就是100、100,而它在我們的新畫布上繪制時應該剛好也是要繪制到左上角的,坐標應該為0,0才對,所以所有的元素坐標均需要減去minx、miny
              element.x -=minx;
              element.y -=miny;
              element.render();
            });
          };
          

          2022-04-27-09-58-18.gif

          當然,我們替換了用來的畫布元素、繪圖上下文等,實際上應該在導出后恢復成原來的,篇幅有限就不具體展開了。

          白白

          作為喜新厭舊的我們,現在是時候跟我們的小矩形說再見了。

          刪除可太簡單了,直接把矩形從元素大家庭數組里把它去掉即可:

          const deleteActiveElement=()=> {
            if (!activeElement) {
              return;
            }
            let index=allElements.findIndex((element)=> {
              return element===activeElement;
            });
            allElements.splice(index, 1);
            renderAllElements();
          };
          

          2022-04-27-10-04-06.gif

          小結

          以上就是白板的核心邏輯,是不是很簡單,如果有下一篇的話筆者會繼續為大家介紹一下箭頭的繪制、自由書寫、文字的繪制,以及如何按比例縮放文字圖片等這些需要固定長寬比例的圖形、如何縮放自由書寫折線這些由多個點構成的元素,敬請期待,白白~

          過canvas可以進行畫圖實現一些動畫效果等,今天練習下通過canvas來實現一個簡易的電子畫板,可以在白板上進行畫畫,然后指定不同的顏色、線條粗細,加載不同的背景以及擦除效果。

          功能劃分

          • 線條涂抹
          • 可以擦除指定區域畫布
          • 清除畫布
          • 可以保存到圖片下載
          • 可以指定不同的顏色的線條
          • 可以指定線條的寬度

          canvas功能點

          根據以上操作,實際上用到的函數比較少,主要是:

          • clearRect 清除某位置區域的畫布
          • drawImage 加載圖片到畫布中
          • lineTo 劃線

          基本上就用到了這么幾個,因為功能比較簡單,所以用到的函數比較少。

          大體思路

          思路的話,其實就是初始化canvas元素,由于需要加載背景圖層,所以設置兩個canvas元素,底層用來加載背景,上層用來繪畫。 用到的事件的話就是onmousedown onmousemove onmouseup 三個事件,初始化的時候監聽下這幾個事件即可。 然后根據event中的坐標,來繪制線條以及清除畫布。

          代碼實現

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>畫板</title>
              <style>
                  html,body{
                      margin:0px;
                      padding: 0px;
                      height:100%;
                      width: 100%;
                      overflow: hidden;
                  }
                  canvas{
                      height:100%;
                      width:100%;
                      position:absolute;
                      left:0px;
                      top:0px;
                      z-index:100;
                      background-color: transparent;
                  }
                  #bg{
                      background-color: #49e;
                      z-index:10;
                  }
                  .tool>span{
                      display:inline-block;
                      position:absolute;
                      width:100px;
                      height:30px;
                      border:1px solid #f2f2f2;
                      text-align:center;
                      line-height:30px;
                      color:#f2f2f2;
                      cursor:pointer;
                  }
                  .draw{left:10px;top:10px;}
                  .clear{left:150px;top:10px;}
                  .empty{left:300px;top:10px;}
                  .tianzige{left:450px;top:10px;}
                  .hengxian{left:600px;top:10px;}
                  .screen{left:750px;top:10px;}
                  .tool{
                      position:absolute;
                      left:0px;
                      top:0px;
                      z-index:99999;
                  }
                  .color{
                      position:absolute;
                      right:0px;
                      top:0px;
                      width:110px;
                      z-index:9999;
                      height:100px;
                  }
                  .color>span{
                      display:inline-block;
                      height:20px;
                      margin:2px;
                      width:50px;
                      line-height:20px;
                      text-align:center;
                      float:left;
                      cursor:pointer;
                      font-size:12px;
                  }
                  .c1{background-color:red;color:white;}
                  .c2{background-color:yellow;color:#333;}
                  .c3{background-color:#333;color:white;}
                  .c4{background-color:green;color:white;}
                  .c5{background-color:white;color:#333;}
                  .c6{background-color:purple;color:#f2f2f2;}
                  .linewidth{
                      position:absolute;
                      top:80px;
                      right:0px;
                      width:110px;
                      height:100px;
                      z-index:09999;
                  }
                  .linewidth>span{
                      width:100%;
                      display:inline-block;
                      margin-bottom:5px;
                      cursor:pointer;
                      background-color:white;
                  }
                  .line1{height:5px;}
                  .line2{height:10px;}
                  .line3{height:20px;}
              </style>
          </head>
          <body>
              <div class="tool">
                  <span class="draw" onclick="draw()">畫筆</span>
                  <span class="clear" onclick="xiangpi()">橡皮</span>
                  <span class="empty" onclick="empty()">清除</span>
                  <span class="tianzige" onclick="tianzige()">田字格</span>
                  <span class="hengxian" onclick="hengxian()">橫線</span>
          
                  <span class="screen" onclick="screena()">截屏</span>
              </div>
              <div class="color">
                  <span class="c1" onclick="changeColor('red')">紅</span>
                  <span class="c2" onclick="changeColor('yellow')">黃</span>
                  <span class="c3" onclick="changeColor('#333')">黑</span>
                  <span class="c4" onclick="changeColor('green')">綠</span>
                  <span class="c5" onclick="changeColor('white')">白</span>
                  <span class="c6" onclick="changeColor('purple')">紫</span>
              </div>
              <div class="linewidth">
                  <span class="line1" onclick="changeWidth(5)"></span>
                  <span class="line2" onclick="changeWidth(10)"></span>
                  <span class="line3" onclick="changeWidth(20)"></span>
              </div>
          </body>
          </html>

          以上是html以及css樣式,主要是針對一些按鈕、設置、樣式進行處理。大體效果如下:

          效果圖

          然后針對canvas進行初始化以及事件綁定處理:

          //初始化幾個參數,后續可以通過用戶點擊按鈕進行改變。
          var status='draw';//'draw' 'clear' 
          var dotWidth=50;
          var color='white';
          var lineWidth=5;
          var canvas={
              //canvas初始化
              init () {
                  this.ele=document.createElement('canvas');
                  document.body.appendChild(this.ele);
                  this.ctx=this.ele.getContext('2d');
                  //背景圖層
                  this.floor=document.createElement('canvas');
                  this.floor.id='bg';
                  document.body.appendChild(this.floor);
                  this.floorCtx=this.floor.getContext('2d');
                  //設定canvas的寬高
                  this.width=this.ele.width=this.floor.width=window.innerWidth;
                  this.height=this.ele.height=this.floor.height=window.innerHeight;
          
                  return this;
              },
              get (){
                  return this;
              },
              //加載背景圖層
              drawImage (imgPath){
                  var that=this;
                  // that.floorCxt.clearRect(0,0,that.width,that.height);
                  var img=new Image();
                  img.src=imgPath;
                  img.onload=function(){
                      that.floorCtx.clearRect(0,0,that.width,that.height);
                      that.floorCtx.drawImage(img,that.width/2 - 500,that.height/2 - 100);
                  }
          
              },
              //事件綁定:鼠標按鈕、鼠標移動、鼠標彈起
              bind(){
                  let ctx=this.ctx;
                  let startDraw=false;//標識是否開始繪制
                  this.ele.onmousedown=function(ev){
                      startDraw=true;
                      var x=ev.clientX,y=ev.clientY;
                      ctx.beginPath();
                  }
                  this.ele.onmousemove=function(ev){
                      if(startDraw){
                          console.log(status);
                          var x=ev.clientX,y=ev.clientY;
                          if(status=='draw'){
                              ctx.strokeStyle=color;
                              ctx.lineWidth=lineWidth;
                              ctx.lineTo(x,y);
                              ctx.stroke();
                          }else if(status=='clear'){
                              //清除
                              ctx.strokeStyle='rgba(0,0,0,1)';
                              ctx.clearRect(x - 40,y - 40,80,80);
                          }
                      }
                  }
                  this.ele.onmouseup=function(){
                      ctx.closePath();
                      startDraw=false;
                  }
              }
          }
          canvas.init().bind();

          核心的繪畫、移動已經可以了,剩下都是一些邊邊角角,增加一些點擊事件即可,包括該表顏色、線條寬度以及操作狀態等。

          //改變全局狀態的顏色
          function changeColor(c){
              color=c;
          }
          //切換繪畫/清除狀態
          function draw(){
              console.log('abc');
              status='draw';
          }
          //切換繪畫/清除狀態
          function xiangpi(){
              console.log('aaa');
              status='clear';
          }
          //改變線條寬度
          function changeWidth(w){
              lineWidth=w;
          }
          //加載背景圖片
          function tianzige(){
              canvas.get().drawImage('bg.png');
          }
          function hengxian(){
              canvas.get().drawImage('line.png');    
          }
          //清空畫布
          function empty(){
              var ins=canvas.get();
              ins.ctx.clearRect(0,0,ins.width,ins.height);
          }
          //保存圖片
          function screena(){
              //設置保存圖片的類型
              var type='jpg';
              var imgdata=canvas.get().ele.toDataURL(type);
              //將mime-type改為image/octet-stream,強制讓瀏覽器下載
              var fixtype=function (type) {
                  type=type.toLocaleLowerCase().replace(/jpg/i, 'jpeg');
                  var r=type.match(/png|jpeg|bmp|gif/)[0];
                  return 'image/' + r;
              }
              imgdata=imgdata.replace(fixtype(type), 'image/octet-stream')
              //將圖片保存到本地
              var saveFile=function (data, filename) {
                  var link=document.createElement('a');
                  link.href=data;
                  link.download=filename;
                  var event=document.createEvent('MouseEvents');
                  event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                  link.dispatchEvent(event);
              }
              var filename=new Date().toLocaleDateString() + '.' + type;
              saveFile(imgdata, filename);
          }

          以上就已經通過canvas實現了一個簡易的電子畫板小工具,當然,目前僅僅是個Demo,用到生產是絕對不行滴。


          主站蜘蛛池模板: 国产韩国精品一区二区三区久久| 精品乱子伦一区二区三区高清免费播放| 亚洲日韩精品国产一区二区三区 | 久久综合一区二区无码| 好吊视频一区二区三区| 欧美av色香蕉一区二区蜜桃小说 | 亚洲愉拍一区二区三区| 国产亚洲自拍一区| 日本一区二区三区四区视频| 久久99精品波多结衣一区| 亚洲一区二区三区免费| 日韩A无码AV一区二区三区| 国产精品久久久久久一区二区三区 | 国产情侣一区二区| 日韩一区二区超清视频| 精品国产鲁一鲁一区二区| 日韩高清国产一区在线| 日韩精品无码一区二区三区 | 免费精品一区二区三区在线观看| 91精品一区二区综合在线| 一区二区三区日韩精品| 91在线视频一区| 韩国福利一区二区美女视频| 国产精品视频一区二区三区经 | 四虎永久在线精品免费一区二区 | 亚洲国产韩国一区二区| 久久久久人妻一区精品色| 久久精品无码一区二区无码| 亚洲国产精品一区二区久| 亚洲电影唐人社一区二区| 亚洲无码一区二区三区| 精品国产日韩亚洲一区91| 日韩毛片一区视频免费| 中文字幕精品亚洲无线码一区应用| 一区二区三区视频网站| 国模私拍一区二区三区| 91国偷自产一区二区三区| 国产乱人伦精品一区二区在线观看| 夜精品a一区二区三区| 精品亚洲一区二区三区在线播放| 亚洲午夜福利AV一区二区无码|