iT邦幫忙

1

Canvas 如何旋轉縮放後再依其結果做裁切?

抱歉各位。最近開始用 Canvas
碰到一個不知道該如何處理的問題。有找了許多範例試著處理。
但一直沒辦法正常解決。

先說說情況。
基本上來說,我因為為了要旋轉的關係。所以有將畫布直接依最大的寬或高,處理成一個正方型。

 var tempCanvas = document.createElement('canvas');
                        var tempCtx = tempCanvas.getContext('2d');
                        
                        //配合旋轉將畫布列
                        if(tempimg.width > tempimg.height){
                            tempCanvas.setAttribute('width', tempimg.width);
                            tempCanvas.setAttribute('height', tempimg.width);
                        }else{
                            tempCanvas.setAttribute('width', tempimg.height);
                            tempCanvas.setAttribute('height', tempimg.height);
                        }

我目前遇到的問題是,旋轉縮放處理後。我會再將其做一個方塊裁切。
但因為tempCanvas已經存在控制項。導致裁切也無法正常對應。

我也試著用如下的方式試試看能否繪入後再做裁切。
但還是無法很正常

 //圖片旋轉處理
                        if(setData.imgData.rotateScore!=0){
                            tempCtx.translate(tempCanvas.width  /  2, tempCanvas.height  /  2);
                            tempCtx.rotate(Math.PI  /  (180/setData.imgData.rotateScore));

                            tempMovX = movX;
                            tempMovY = movY;

                            movX = tempCanvas.width/-2 + tempMovX;
                            movY = tempCanvas.height/-2 + tempMovY;
                        }
                        tempCtx.drawImage(tempimg, movX, movY);
                        
                        //進行裁切處理
                        tempCtx.beginPath();
                        tempCtx.rect(clipX, clipY, clipW, clipH);
                        tempCtx.clip();
                        tempCtx.drawImage(tempCanvas, 0, 0);

想問問會 Canvas 的大大們。
我該如何改,才能達到我想要的旋轉縮放處理後的圖,再做裁切。

ccutmis iT邦高手 2 級 ‧ 2021-04-20 20:30:17 檢舉
http://jsfiddle.net/m1erickson/6ZsCz/
你這只有旋轉。旋轉本身沒問題。
我的問題主要是 旋轉後,無法正常裁切。
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 個回答

5
淺水員
iT邦大師 6 級 ‧ 2021-04-20 21:04:34
最佳解答

我是 canvas 一開始就設定好要裁切的大小
算出需要的矩陣後,直接用 drawImage 把圖畫上去

/**
 * 先將圖檔以(x, y)為中心,旋轉 deg 度
 * 再以 (clipX, clipY) 為左上角座標,裁切出 clipW x clipH 的 canvas
 * 
 * @param {image} img imageElement
 * @param {int} x 旋轉中心 x 座標
 * @param {int} y 旋轉中心 y 座標
 * @param {number} deg 旋轉角度
 * @param {int} clipX 裁切左上角 x 座標
 * @param {int} clipY 裁切左上角 y 座標
 * @param {int} clipW 裁切後的寬度
 * @param {int} clipH 裁切後的高度
 * @returns {canvas} canvas物件
 */
function getCanvasWithClip(img, x, y, deg, clipX, clipY, clipW, clipH) {
    let cvs=document.createElement('canvas');
    let ctx=cvs.getContext('2d');
    cvs.width=clipW;
    cvs.height=clipH;
    //預存 sin 跟 cos 值
    let c=Math.cos(Math.PI*deg/180);
    let s=Math.sin(Math.PI*deg/180);
    //設定轉換矩陣
    ctx.transform(c, s, -s, c, -c*x+s*y+x-clipX, -s*x-c*y+y-clipY);
    ctx.drawImage(img, 0, 0);
    return cvs;
}

使用範例

let img=new Image();
img.addEventListener("load", function() {
    let w=parseInt(this.width, 10);
    let h=parseInt(this.height, 10);
    let cvs=getCanvasWithClip(this, w/2, h/2, 30, w/2-30, h/2-30, 60, 60);
    document.getElementById('draw').appendChild(cvs);
});
img.src='test.jpg';

矩陣是這樣算出來的
https://ithelp.ithome.com.tw/upload/images/20210420/20112943KAt1ylkFxS.png

看更多先前的回應...收起先前的回應...

我沒說明清楚。
因為我目前設計的是合成的。
是數張小圖,會做縮放旋轉,並切好適合大小後。

再跟一張大圖合成。

也就是在小圖的生成中,就需要先切好大小。
要不然會跟其它圖相疊到。

縮放旋轉我已經是可以辦到了。
在不旋轉的情況下。我還可以正常裁切我需要的大小。

但一但旋轉後,我怎麼裁切都不對。目前我的問題點在這邊。
有點頭痛。

我是已經有想過,再生一個畫布出來處理。
但還是希望了有一次性的寫法來處理。

淺水員 iT邦大師 6 級 ‧ 2021-04-21 11:56:11 檢舉

裁切出來的長方形,相對螢幕是傾斜的還是正的?

正的。
也就是縮放旋轉後的畫面。我會再用一個長方形裁切一次。

淺水員 iT邦大師 6 級 ‧ 2021-04-21 13:24:44 檢舉

我是已經有想過,再生一個畫布出來處理。

我原本寫的函式可以生出 canvas
這個 canvas 可以當作 image,貼到要輸出的 canvas

因為上面矩陣的計算比較抽象
我後來發現可以改成 translate、rotate、… 的組合
下面這個函式順便添加了 scale 參數

/**
 * 先將圖檔以(x, y)為中心,縮放 scale 倍,再旋轉 deg 度
 * 接著以 (clipX, clipY) 為左上角座標,裁切出 clipW x clipH 的 canvas
 * 
 * @param {image} img imageElement
 * @param {int} x 旋轉中心 x 座標
 * @param {int} y 旋轉中心 y 座標
 * @param {number} deg 旋轉角度
 * @param {number} scale 縮放倍率
 * @param {int} clipX 裁切左上角 x 座標
 * @param {int} clipY 裁切左上角 y 座標
 * @param {int} clipW 裁切後的寬度
 * @param {int} clipH 裁切後的高度
 * @returns {canvas} canvas物件
 */
function getCanvasWithClip(img, x, y, deg, scale, clipX, clipY, clipW, clipH) {
    let cvs=document.createElement('canvas');
    let ctx=cvs.getContext('2d');
    cvs.width=clipW;
    cvs.height=clipH;
    //設定轉換矩陣
    ctx.translate(x-clipX, y-clipY);
    ctx.rotate(Math.PI*deg/180);
    ctx.scale(scale, scale);
    ctx.translate(-x, -y);
    //畫圖
    ctx.drawImage(img, 0, 0);
    return cvs;
}

使用範例

let img=new Image();
img.addEventListener("load", function() {
    let w=parseInt(this.width, 10);
    let h=parseInt(this.height, 10);
    //主要的 canvas
    let mainCanvas=document.createElement('canvas');
    mainCanvas.width=600;
    mainCanvas.height=600;
    let ctx=mainCanvas.getContext('2d');
    //以(w/2, h/2)為中心,縮小為50% 後旋轉 30 度,再裁切出中心區域 100 x 100 的圖片
    let clipedCanvas=getCanvasWithClip(this, w/2, h/2, 30, 0.5, w/2-50, h/2-50, 100, 100);
    //將剛剛切出來的圖片貼到 (100,100) 的位置
    ctx.drawImage(clipedCanvas, 100,100);
    document.getElementById('draw').appendChild(mainCanvas);
});
img.src='test.jpg';

PS. 如果是要傾斜的,我有其他方式可以不用產生新的 canvas。如果是正的,那直接產生 canvas 會比較簡單。不然的話可能要用ImageData做 buffer,我猜效能不見得會比較好。

目前參照你後面的寫法。可以勉強輸出。
主要是你上面的矩陣的計算方式給我很大的幫助。

不過目前輸出出來的圖,還是偏移很大。我還在查看計算的方式是否正確。

因為我原本的寫法。在還沒旋轉。其實輸出還算正常。
只有旋轉會跑掉。
不過現在用你的函式是全跑掉了@@"

我提供一下我的完整程式碼給你看好了。

//載入生成區 2000*1000
            const ca = document.getElementById("prewImgOk");
            const ctx = ca.getContext("2d");
	//清空畫布
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.clearRect(0, 0, ca.width, ca.height);
            const mainImg = document.getElementById("preview"); //帶入主圖資料

            //預覽圖的寬高
            var preVIewW = $("#design-result img").eq(0).width();
            var preVIewH = $("#design-result img").eq(0).height();
            var preVIewTop = $("#design-result img").eq(0).offset().top;
            var preVIewLeft = $("#design-result img").eq(0).offset().left;

            //生成區的寬高
            var prewImgOkW = ca.width;
            var prewImgOkH = ca.height;            
	
            var imgPk = prewImgOkW / preVIewW;    //圖片之間的縮放比例            

	//進行多個貼圖合成
            $.each(window.designTemplate.uploadData, function (i, v) {                
	//判斷是否有圖片資料,有的話才處理貼圖
                if (v.imgData) {
	//獲取貼圖區塊的位置及寬高
                    var imgBoxX = $(".img-uploaded").eq(i).offset().left;
                    var imgBoxY = $(".img-uploaded").eq(i).offset().top;
                    var imgBoxW = $(".img-uploaded").eq(i).width();
                    var imgBoxH = $(".img-uploaded").eq(i).height();

                    var tempimg = new Image();//生成新的IMG元件
			//圖片載入應用處理
                    tempimg.onload = function () {
                        var setData = v;//獲得指定設定值
                        
			//暫時貼圖畫布區
                        var tempCanvas = document.createElement('canvas');
                        var tempCtx = tempCanvas.getContext('2d');
                        
                        //配合旋轉將畫布列
                        if (tempimg.width > tempimg.height) {
                            tempCanvas.setAttribute('width', tempimg.width);
                            tempCanvas.setAttribute('height', tempimg.width);
                        } else {
                            tempCanvas.setAttribute('width', tempimg.height);
                            tempCanvas.setAttribute('height', tempimg.height);
                        }
                        //移位比例計算
                        var movePv = tempCanvas.width / 300;
                        var clipX = (((clipW - tempimg.width) / 2) / setData.imgData.scaleScore) - (setData.imgData.moveScore.x * movePv);
                        var clipY = (((clipH - tempimg.height) / 2) / setData.imgData.scaleScore) - (setData.imgData.moveScore.y * movePv);
                        var clipW = tempimg.width * setData.imgData.scaleScore;
                        var clipH = tempimg.height * setData.imgData.scaleScore;

                        //這邊是先帶入你後續的函式做處理
                        clipedCanvas=getCanvasWithClip(tempimg, tempimg.width/2, tempimg.height/2, setData.imgData.rotateScore, setData.imgData.scaleScore, tempimg.width/2, tempimg.height/2, tempimg.width, tempimg.height);
                        tempCtx.drawImage(clipedCanvas, 0, 0);
                        //以下是我原本的寫法
                        /*
                        //目前圖片的處理
                        var clipW = tempimg.width * setData.imgData.scaleScore;
                        var clipH = tempimg.height * setData.imgData.scaleScore;



                        //進行XY定位裁切計算
                        var clipX = (((clipW - tempimg.width) / 2) / setData.imgData.scaleScore) - (setData.imgData.moveScore.x * movePv);
                        var clipY = (((clipH - tempimg.height) / 2) / setData.imgData.scaleScore) - (setData.imgData.moveScore.y * movePv);


                        var clipW = tempCanvas.width / setData.imgData.scaleScore;
                        var clipH = tempCanvas.height / setData.imgData.scaleScore;
                        console.log(setData.imgData.scaleScore, 'setData.imgData.scaleScore');
                        console.log(setData.imgData.moveScore, 'setData.imgData.moveScore');

                        console.log(clipW, 'clipW');
                        console.log(clipH, 'clipH');
                        console.log(clipX, 'clipX');
                        console.log(clipY, 'clipY');

                        //置中處理
                        var movX = 0;
                        var movY = 0;
                        if (tempimg.width > tempimg.height) {
                            movY = (tempimg.width - tempimg.height) / 2;
                        } else {
                            movX = (tempimg.height - tempimg.width) / 2;
                        }

                        console.log(clipW, 'editclipW');
                        console.log(clipH, 'editclipH');
                        console.log(clipX, 'editclipX');
                        console.log(clipY, 'editclipY');

                        //圖片旋轉處理
                        if (setData.imgData.rotateScore != 0) {
                            let deg = setData.imgData.rotateScore;
                            //cvs=getCanvasWithClip(tempimg, tempCanvas.width/2, tempCanvas.height/2, deg, clipX, clipY, clipW, clipH);
                            cvs = getCanvasWithClip(tempimg, tempCanvas.width / 2 + movX, tempCanvas.height / 2 + movY, deg, clipX, clipY, clipW, clipH);
                            tempCtx.drawImage(cvs, 0, 0);
                            /*

                            //預存 sin 跟 cos 值
                            let deg = setData.imgData.rotateScore;
                            let c=Math.cos(Math.PI*deg/180);
                            let s=Math.sin(Math.PI*deg/180);

                            tempMovX = movX;
                            tempMovY = movY;

                            movX = tempCanvas.width/-2 + tempMovX;
                            movY = tempCanvas.height/-2 + tempMovY;

                            //設定轉換矩陣
                            tempCtx.transform(c, s, -s, c, -c*movX+s*movY+movX-clipX, -s*movX-c*movY+movY-clipY);
                            tempCtx.drawImage(tempimg, 0, 0);


                            //tempCtx.translate(tempCanvas.width  /  2, tempCanvas.height  /  2);
                            //tempCtx.rotate(Math.PI  /  (180/setData.imgData.rotateScore));


                        } else {
                            tempCtx.beginPath();
                            tempCtx.rect(clipX, clipY, clipW, clipH);
                            tempCtx.clip();
                            tempCtx.drawImage(tempimg, movX, movY);
                        }
                        */
                        
                        //進行大圖合成

                        //處理寬高
                        var sw = (prewImgOkW * ((setData.width) * 0.01));
                        var w = (prewImgOkW * ((setData.width) * 0.01)) * setData.imgData.scaleScore;
                        console.log(setData.width, 'v.width');
                        var wpk = w / tempCanvas.width;
                        var swpk = sw / tempCanvas.width;

                        var sh = tempCanvas.height * swpk;
                        var h = tempCanvas.height * wpk;

                        var setX = (prewImgOkW * setData.position.left * 0.01) - 325;
                        var setY = prewImgOkH * setData.position.top * 0.01 - preVIewH;

                        setX -= (w - sw) / 2;
                        setY -= (h - sh) / 2;
                        
                        ctx.drawImage(tempCanvas, setX, setY, w, h);//最後將暫存圖貼入                        
                    };
                    
                    tempimg.src = v.imgData.origin;//載入對應圖片
                }
            });
            //ctx.drawImage(mainImg,0,0);

            setTimeout(function () {
                ctx.drawImage(mainImg, 0, 0);//這邊是將最後的主圖做最後處理,暫時用的
            }, 2000);

目前我用你最後的函數處理。
定位調整一下參數後。已經有調整成功了。

雖然還不太明白一件事。

//將剛剛切出來的圖片貼到 (100,100) 的位置
    ctx.drawImage(clipedCanvas, 100,100);

這邊為何需要 100 100 的定位。

而且還有點小問題。旋轉後的圖案。直向圖還OK。但橫向圖還是會被壓扁。我目前還在查看這一部份。

目前是在思考將函數內的畫布。是否要將其化成正方型來處理。
化成正方型的畫布在90度時不會被壓扁。但截圖的位置還是有跑掉。
我還在試著要怎麼調整才對。

淺水員 iT邦大師 6 級 ‧ 2021-04-21 19:10:02 檢舉

那個 (100, 100) 只是做為範例,數值可以依照需求改。

我這邊比較難回答的原因是無法完全了解需求,晚點我整理一下多個轉換疊加後要怎麼看。這樣也許比較幫得上忙。

要說明的話,其實是一個印刷圖。
有一張主要的圖,你可以將其視為一種佈景。
其上面會有5~8個區塊。可以自行上傳相片貼上去。
而上傳的相片,可以調整縮放旋轉。

在編輯過程中,我是利用單純的DIV跟CSS處理。
最後確定後,才會開始組合生圖。
所以程式上你看到的一些XY值。其實都是對應DIV上的圖層位置計算過來的。

我那段程式就是在做組合生圖的動作。
由於需要高解析。所以其畫布預設都很大。

主佈景圖是一個PNG。區塊的部份都是做成透空。
所以我在組合圖是先將相片圖貼好位置。再將佈景圖蓋上去。

其實還有一個最後動作是文字繪上。不過這部份我有處理好了。

淺水員 iT邦大師 6 級 ‧ 2021-04-21 20:45:21 檢舉
淺水員 iT邦大師 6 級 ‧ 2021-04-22 21:12:12 檢舉

我後來有找到不用另外建立 canvas 的方法
透過 save、clip、restore 可以滿足需求

做出來大概像這樣
https://ithelp.ithome.com.tw/upload/images/20210422/20112943AF9PwFIeD7.png

程式碼

const opts=[
    {//路奇(綠色帽子)
        src: { //人物在圖片的座標
            x: 160,
            y: 93,
        },
        dest: { //要放到 canvas 的座標
            x: 50,
            y: 100,
        },
        rot: -60, //旋轉角度
        clipRadius: 50 //裁切圓半徑
    },{//恐龍
        src: {
            x: 288,
            y: 102,
        },
        dest: {
            x: 150,
            y: 100,
        },
        rot: 180,
        clipRadius: 50
    },{//馬力歐(紅色帽子)
        src: {
            x: 468,
            y: 123,
        },
        dest: {
            x: 250,
            y: 100,
        },
        rot: 60,
        clipRadius: 50
    }
]

function draw(ctx, img, opt) {
    ctx.save(); //儲存狀態
    ctx.translate(opt.dest.x, opt.dest.y); //[C] 將原點移動到目的地
    
    //裁切時,座標的判斷會套用[C]矩陣
    //這邊用圓形只是方便示範而已,可以改任何形狀
    ctx.beginPath();
    ctx.arc(0, 0, opt.clipRadius, 0, Math.PI*2, true);
    ctx.clip();

    ctx.rotate(opt.rot*Math.PI/180); //[B] 旋轉指定角度
    ctx.translate(-opt.src.x, -opt.src.y); //[A] 移動到原點
    
    //畫圖時,會套用 [C][B][A] 矩陣的乘積(注意[A]會先作用,最後才是[C])
    ctx.drawImage(img, 0, 0);
    
    ctx.restore(); //回復之前儲存狀態,因此剛剛的 clip、rotate、translate等設定都會被取消
}

let img=new Image();
img.addEventListener("load", function() {
    let cvs=document.querySelector('canvas');
    cvs.width=300;
    cvs.height=200;
    let ctx=cvs.getContext('2d');
    //依照設定把圖片變換後畫到 canvas
    opts.forEach((opt)=>{
        draw(ctx, img, opt);
    });
});
img.src=document.querySelector('img').src;

完整範例

這個看起來能用喔。也比較直覺。

謝啦!!

我要發表回答

立即登入回答