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

一、介紹

 

  canvas 已經出來很久了,相信你們多少都有接觸。html

  若是你是前端頁面開發/移動開發,那麼你確定會有作過圖片上傳處理,圖片優化,以及圖片合成,這些都是能夠用 canvas 實現的。前端

  若是你是作前端遊戲開發的,可能會很是熟悉,或者說對幾何和各類圖形變化很是瞭解。node

  這裏我介紹的是簡單的、基本的,可是很是徹底的一個 2d 的 canvas 案例。npm

  基本上了解了這些,全部的 canvas 中的 2d 變化基本均可以會了。canvas

 

  先來一個截圖看看效果:api

 

 

  如上面所看,能夠總結出幾個功能點:session

 

    一、添加多張圖片或者文字到 canvas 。( 這裏沒有添加文字,咱們能夠先把文字利用canvas轉爲圖片,而後添加 canvas 上 )app

    二、圖片的縮放,根據選擇不一樣的點實現不一樣縮放函數

    三、圖片移動,改變圖片在 canvas 的中心位置測試

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

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

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

    七、初始化數據,經過以前保存的數據,從新繪製。

 

 

  代碼案例介紹: 

    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 包沒有測試,本地的能夠實現各類方法了。若有問題能夠留言。。

 

二、項目開發

  

  知識梳理:

 

  在開發中咱們須要不少關於平面幾何的知識來處理咱們的操做,例如:

  

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

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

    三、計算某個向量在另外一個向量上面的距離 : 用於旋轉以後,的移動距離計算

    四、某點繞道某點旋轉必定角度的點 : 用於肯定旋轉後的點的位置

    

  是否是腦子裏浮現了不少高中初中的數學幾何公式。

  若是沒有,百度下吧,都是不少有意思的公式,讓本身重溫下高中數學,回憶一下高中。

  證實一下本身學太高中數學。

  

  

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

  

  如下若是須要查看,最好下載源碼對照的查看

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

  

  一、首先建立一個 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  數據。用於存儲數據和保存

 

 

 

  二、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 實例進行繪製。

 

  

  三、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 方法重繪製。

 

 

  四、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 : 判斷時候在對應的操做點上,並返回對應的操做點名稱

 

  

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

 

http://www.javashuo.com/article/p-zhpspoqe-bk.html 

   個人博客 :  XiaoLong's Blog

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

相關文章
相關標籤/搜索