圖片在 canvas 中的 選中/平移/縮放/旋轉,包含了所有canvas的2D變化,讓你認識到數學的重要性


1、介紹

 

  canvas 已經出來好久了,相信大家多少都有接觸。

  如果你是前端頁面開發/移動開發,那么你肯定會有做過圖片上傳處理,圖片優化,以及圖片合成,這些都是可以用 canvas 實現的。

  如果你是做前端游戲開發的,可能會非常熟悉,或者說對幾何和各種圖形變化非常了解。

  這里我介紹的是簡單的、基本的,但是非常完全的一個 2d 的 canvas 案例。

  基本上了解了這些,所有的 canvas 中的 2d 變化基本都可以會了。

 

  先來一個截圖看看效果:

 

 

  如上面所看,可以總結出幾個功能點:

 

    1、添加多張圖片或者文字到 canvas 。( 這里沒有添加文字,我們可以先把文字利用canvas轉為圖片,然后添加 canvas 上 )

    2、圖片的縮放,根據選擇不同的點實現不同縮放

    3、圖片移動,改變圖片在 canvas 的中心位置

    4、圖片旋轉,根據旋轉點在移動的角度進行旋轉

    5、圖片選擇,兩種方式:一種根據圖片的位置,確定當前選擇的圖形,第二種是點擊列表選擇

    6、數據的保存,提供了保存按鈕,保存圖形的位置和大小以及旋轉角度

    7、初始化數據,通過之前保存的數據,重新繪制。

 

 

  代碼案例介紹: 

    html 代碼:

  

<canvas height="960" width="960" style="width: 100%;" id="test"></canvas>
<div id="list"></div>
<button id="save">保存</button>

  

  js代碼是模塊形式開發的,並且傳到 npm 上面,可以自行下載並且有源碼:

 

yarn add xl_canvas

  

  代碼調用和實現:

import Canvas from 'xl_cnavas';

 const dataCa =   sessionStorage.getItem('test_tst_111');
    const canvas = new Canvas({
        canvas: 'test',
        target: 'test',
        list: 'list',
        height: 960,
        width: 960,
        data: dataCa?JSON.parse(dataCa):[],
    });
    document.getElementById('save').addEventListener('click', () => {
        sessionStorage.setItem('test_tst_111',
            JSON.stringify(canvas.save()));
    });

    // canvas.addPhoto('https://cdn.eoniq.co/spree/images/283205/desktop/CI-26-LS_b6bb28a3914ae9caa651abbddb548054.jpg?1533196945');
    // canvas.addPhoto('http://www.runoob.com/wp-content/uploads/2013/11/img_the_scream.jpg');

  

  npm 包沒有測試,本地的可以實現各種方法了。如有問題可以留言。。

 

2、項目開發

  

  知識梳理:

 

  在開發中我們需要很多關於平面幾何的知識來處理我們的操作,例如:

  

    1、確定某個點是否在矩形內   : 用於確定點擊時候選中的圖形

    2、計算向量的角度 : 用於處理旋轉

    3、計算某個向量在另一個向量上面的距離 : 用於旋轉之后,的移動距離計算

    4、某點繞道某點旋轉一定角度的點 : 用於確定旋轉后的點的位置

    

  是不是腦子里浮現了很多高中初中的數學幾何公式。

  如果沒有,百度下吧,都是很多有意思的公式,讓自己重溫下高中數學,回憶一下高中。

  證明一下自己學過高中數學。

  

  

  代碼設計/簡要開發介紹:

  

  以下如果需要查看,最好下載源碼對照的查看

  如何開始這個功能的開發呢?

  

  1、首先創建一個 Canvas  類

 

constructor(options) {
        this.options = options;
        const {
            canvas,
            height,
            width,
            target,
            before,
            after,
            data = [],
            list = null,
        } = this.options;
        this.canvas = null; // 畫布
        this.height = height; // 畫布的寬高
        this.width = width;
        this.target = target;
        this.before = before;
        this.after = after;
        this.data = data;
        this.layers = []; // 畫布的層
        if (typeof canvas === 'string') {
            this.canvas = document.getElementById(canvas);
        } else {
            this.canvas = canvas;
        }
        if (typeof target === 'string') {
            this.target = document.getElementById(target);
        } else {
            this.target = target;
        }
        if (typeof list === 'string') {
            this.list = document.getElementById(list);
        } else {
            this.list = list;
        }
        this.canvas.width = width;
        this.canvas.height = height;
        this.context = this.canvas.getContext('2d'); // 畫布對象
        this.loaded = 0;
        this.border = new Border(this);
        this.current = null;
        this.init();
        this.initEvent();
    }

  

  這是 canvas 類的構造函數,這里接受有參數:

 

    canvas : 傳入 canvas 對象或者當前 html 的元素的 id,以供整個功能的開發。

    height / width : 寬和高,整個繪制過程中,寬和高都是這個為基准

    target : 這個是用來接受事件的元素。這個應該和 canvas 對象的元素寬高相等

    before / after :當初始化數據到時候,會知道初始化數據之前操作和初始化之后操作

    data : 繪制的數據

 

 

  重要的屬性:

  

    layers :添加到畫布的圖形,類似圖層。

    context : canvas 的上下文,用來繪制的 api 集合

    border : 繪制的骨架,當選中某一個圖形的時候,會出現外層的骨架。( 這個單獨創建一個類 )

    current :當前的圖形,也可以理解為當前的圖層。

 

  主要方法介紹(介紹幾個重要的):

    

  addPhoto 方法:

// 添加圖片
    addPhoto(image) {
        if (typeof image === 'string') {
            this.loaded += 1;
            const lyr = new Photo(image, this, () => {
                setTimeout(() => {
                    this.loaded -= 1;
                    if (this.loaded < 1) {
                        this.draw();
                    }
                }, 100);
            });
            this.layers.push(lyr);
            this.addItem(image, lyr.id);
        } else {
            const lyr = new Photo(image, this);
            this.layers.push(lyr);
            this.addItem(image, lyr.id);
            this.draw();
        }
    }

  

  這里是添加 Photo 的方法,其中 photo 是用 Photo 類創建實例的。

  可以先看一下下面介紹的 Photo 類,可以更好了解開發過程。

 

  draw方法(用來觸發繪制):

  draw() {
        this.clear();
        this.layers.forEach((item) => {
            if (typeof item === 'function') {
                item.apply(null, this.context, this.canvas);
            } else {
                item.draw();
            }
        });
        if (this.current) {
            this.border.refresh(this.current.rect);
        }
    }

  

  上面代碼是來繪制 layers 的圖層到 canvas 上。

  這里會判斷 layers 中是否是圖層,如果是圖層才會繪制圖層

  如果不是,就會直接執行方法,該方法傳入的當前的 canvas 這個實例。

  也可以繪圖案到 canvas 上,這樣就可以實現層級關系。

  

  上面做了一個判斷,就是是否繪制 border ,在有選中的情況下會繪制 骨架的

  即 調用 border 的 refresh 方法。在這里可以先去看看 Border 類。( 下面有介紹 )

 

  initEvent 方法(用於綁定方法):

    initEvent() {
        this.target.addEventListener('mousedown', (e) => {
            let p_x = e.pageX;
            let p_y = e.pageY;
            const position = getDocPosition(this.target);
            const scale = this.width / this.target.offsetWidth;
            const point = [
                (p_x - position.x) * scale,
                (p_y - position.y) * scale,
            ];
            const status = this.selectPhoto(point);
            if (status) {
                const move = (event) => {
                    const m_x = event.pageX;
                    const m_y = event.pageY;
                    const vector = [(m_x - p_x) * scale, (m_y - p_y) * scale];
                    if (status === 1) {
                        this.current.rect.translate(vector);
                    } else if (status === 'r_point') {
                        const e_point = [(m_x - position.x) * scale, (m_y - position.y) * scale];
                        const angle = Canvas.getAngle(
                            this.current.rect.center,
                            this.border.r_point,
                            e_point,
                        );
                        if (!isNaN(angle)) {
                            this.current.rect.rotate(angle);
                        } else {
                            return;
                        }
                    } else {
                        this.current.rect.zoom(status, vector);
                    }
                    this.draw();
                    p_x = m_x;
                    p_y = m_y;
                };
                this.target.addEventListener('mousemove', move);
                this.target.addEventListener('mouseup', () => {
                    this.target.removeEventListener('mousemove', move);
                });
            }
        });
        this.list.addEventListener('click', (e) => {
            if (e.target && e.target.nodeName.toUpperCase() === 'IMG') {
                const id = parseInt(e.target.getAttribute('data-id'));
                this.layers.forEach((item, index) => {
                    if (item.id === id) {
                        this.chooseItem(index);
                    }
                });
            }
        });
    }

  

  這個是給 target 對象綁定事件,通過對事件的不同處理來就觸發不同的方法。

  都是直接改變當前的 current 上面的 rect 數據,然后重新繪制。

 

    圖形的選取 : selectPhoto 方法調用,當選中的時候就會設置當前的 current 的圖層

    圖形的移動 : move 方法調用,移動圖層

    圖形的縮放 : zoom 方法調用,接受不同的縮放形式

    圖形的旋轉 : rotate 方法調用,接受角度進行旋轉

 

  其他的方法:

 

  addItem : 向 list 元素對象中添加元素

  selectPhoto :判斷當前的位置確定選中的 Photo

  chooseItem :用於 list 元素中的選取

  clear : 清楚 canvas 畫布

  save : 返回 rect  數據。用於存儲數據和保存

 

 

 

  2、Photo 類

 

constructor(image, canvas, load) {
        this.canvas = canvas;
        this.img = image;
        this.load = load;
        this.id = new Date().getTime();
        this.isLoad = false;
        if (image.rect) {
            this.options = image;
            this.img = this.options.img;
            this.id = this.options.id;
        }
        this.pre();
    }

  

  還是看構造函數,介紹屬性和方法:

    

    canvas : 就是相當於繼承來的,或者是說 canvas 要全局使用

    image :可能是對象,也可以能是 資源地址,但是大多數應該是資源地址

    id : photo 的 id,用於查找和選擇等

    rect :這個是重要的,photo 的數據,如:坐標/寬高/角度等 

 

  稍后介紹 rect 類,先介紹下 photo 的方法:

  

  用於創建 rect 的init方法:

 init() {
        if (this.load) this.load();
        if (this.options) {
            const {
                width, height, center, angle,
            } = this.options.rect;
            this.rect = new Rect(width,
                height, [center[0], center[1]], angle);
            return;
        }
        this.rect = new Rect(this.image.width,
            this.image.height, [this.canvas.width / 2, this.canvas.height / 2], 0);
    }

  

  每次 new Photo 都會創建了一個 ract 實例,作為它的數據存儲 this.rect 。

  每次創建一個 Photo 的時候並且加入到 canvas 的 layers 中的時候並沒有開始繪圖

  繪圖需要調用 Photo 的 draw 方法來觸發,如下:

 

  draw() {
        const { image, canvas, rect } = this;
        const { context } = canvas;
        const points = rect.point;
        const [c_x, c_y] = rect.center;
        context.save();
        context.translate(c_x, c_y);
        context.rotate(rect.angle);
        context.drawImage(image, 0, 0, image.width, image.height,
            points[0][0] - c_x,
            points[0][1] - c_y,
            rect.width,
            rect.height);
        context.restore();
    }

  

  在 canvas 實例調用 draw 方法時候,會一次繪制 layers 中的所有 photo 實例進行繪制。

 

  

  3、rect 類

 

 constructor(width, height, center, angle) {
        this.height = height;
        this.width = width;
        this.center = center;
        this.angle = angle;
        this.point=[]
        this.getPoint();
    }

  

  這里是通過傳入 width / height /center / angle 來確定和初始化 photo 在 canvas 上的輸出。

  

    height / width  : 這是圖形的寬高

    center : 圖形的中間位置

    angle :很顯熱,是圖形旋轉的角度

    point : 四個頂點的位置

 

  一個圖形,有了這個寫數據,基本上能在 canvas 確定位置、大小以及各種形變。

   

  rect 實例的方法:

 

 

  代碼有點多,就簡要介紹吧!

  我們的操作實際上都是操作 rect 的數據。

  一些判斷也是於 rect 數據做對比,或者計算 rect 對象里面的數據。

 

    rotate : 旋轉后 rect 的頂點位置的計算

    translate : 移動后中點位置計算和頂點位置計算

    zoom : 縮放后頂點和中點的位置計算

    isPointInRect : 是否在 Rect 的四個頂點里面

 

  Rect 的類基本介紹完畢了。每次改變后調用 canvas 的 draw 方法重繪制。

 

 

  4、Border 類 

 

  查看這個類最好先瀏覽下 rect 類 和 Photo 類

 

 constructor(canvas) {
        this.canvas = canvas;
    }

    

  這里創建只是獲取到了全局的 canvas 實例。用於后面調用

  

  refresh 方法:  

    refresh(rect) {
        this.rect = rect;
        this.point = this.rect.point;
        // 中點
        this.c_point = [];
        this.point.reduce((a, b) => {
            this.c_point.push([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
            return b;
        }, this.point[3]);
        // 旋轉點
        this.r_point = [(this.point[0][0] + this.point[1][0]) / 2,
            this.point[0][1] - 35];
        this.draw();
    }

  

  這里是接受 rect 的數據,

  然后通過 rect 數據,得到頂點 / 各個線上的中點 / 旋轉點

  調用 refresh 之后就會執行 draw 方法:

 draw() {
        const {
            point,
            center,
            angle,
            width,
            height,
        } = this.rect;
        const { context } = this.canvas;
        const [c_x, c_y] = center;
        const points = point;
        context.save();
        context.translate(c_x, c_y);
        context.rotate(angle);
        context.beginPath();
        context.lineWidth = '2';
        context.strokeStyle = '#73BFF9';
        context.rect(points[0][0] - c_x,
            points[0][1] - c_y,
            width,
            height);

        const pointList = points.concat(this.c_point);
        pointList.push(this.r_point);
        pointList.forEach((item) => {
            const [x, y] = item;
            context.fillStyle = '#73BFF9';
            context.fillRect(x - 6 - c_x, y - 6 - c_y, 12, 12);
        });

        context.moveTo((points[0][0] + points[1][0]) / 2 - c_x,
            points[0][1] - c_y);
        context.lineTo(this.r_point[0] - c_x, this.r_point[1] - c_y);
        context.stroke();
        context.closePath();
        context.restore();
    }

  

  可以看到這里是繪制,並且繪制都是依賴 rect 的數據。

  所以我們並不需要處理旋轉 / 移動 / 縮放等操作,因為每次修改后 rect 數據就會變。

 

 

   isPointInSkeletion : 判斷時候在對應的操作點上,並返回對應的操作點名稱

 

  

 介紹完畢,簡要的介紹開發的設計和流程。如需諒解,請看看源碼。。

 

https://www.cnblogs.com/jiebba/p/9667600.html 

   我的博客 :  XiaoLong's Blog

   博客園小結巴巴: https://www.cnblogs.com/jiebba

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM