iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
Modern Web

無限...寶石?畫布啦!系列 第 11

Day 11 | 觸控的平移

  • 分享至 

  • xImage
  •  

day 11 banner

今天我們要接續實作平移,主要是觸控的部分。

我會示範一種觸控平移的操作:一隻手指頭滑動。

這個會是比較簡單但是可以示範基本的觸控操作可以怎麼實作的範例。

之後如果你想要有其他變化的話可以再從這個基礎往後延伸。

觸控主要會有幾個 event 是我們會需要用到的: touchstarttouchendtouchcancel、以及touchmove

實作邏輯也是跟滑鼠鍵盤還有觸控板類似。

就是需要紀錄一個平移開始的起始點,以及手指滑動到的終點,然後去計算他們之間的差距,並且將相機平移。

首先我們先在 src 裡面建立一個新的檔案 touch-input.ts

然後加入一個新的 class TouchInput。並且給他一個空的 constructor。

touch-input.ts

class TouchInput {
    
    constructor(){

    }
}

我們也是先實作 touchstart event 的 handler,去啟動我們平移的流程。

touch-input.ts

class TouchInput {

    // 上略
    touchstartHandler(event: TouchEvent){

    }
    // 下略
}

在平移的時候,我們的操作邏輯是一隻手指頭按住移動。因此我們只需要處理一隻手指頭的 event ,兩隻手指頭或以上的 event 會留給縮放以及其他操作。

我們先篩選只需要 1 個觸控點的 event。

touch-input.ts

class TouchInput {

    // 上略
    touchstartHandler(event: TouchEvent){
        if(event.targetTouches.length == 1){
            
        }
    }
    // 下略
}

接下來,我們跟滑鼠鍵盤一樣需要紀錄一開始點下的位置。

我們需要一個 instance variable panStartPoint

還有另外一個 instance variable isPanning 去紀錄是否是在平移模式中。

一樣要先記得從 ./vector import Point 這個 type 。

touch-input.ts

import { Point } from "./vector";

class TouchInput {

    private panStartPoint: Point;
    private isPanning: boolean;

    constructor(){
        this.panStartPoint = {x: 0, y: 0};
        this.isPanning = false;
    }

    touchstartHandler(event: TouchEvent){
        if(event.targetTouches.length == 1){
            this.panStartPoint = {x: event.targetTouches[0].clientX, y: event.targetTouches[0].clientY};
            this.isPanning = true;
        }
    }
}

接下來我們也是要跟 KeyboardMouseInput 一樣的流程,當手指開始移動時 touchmove 這個 event 會被觸發。

因此我們在 TouchInput 加上一個 touchmoveHandler

因為手指滑動也很容易觸發重新整理或是上一頁的動作,所以我們也要加上 event.preventDefault()

touch-input.ts

class TouchInput {
    
    // 上略

    touchmoveHandler(event: TouchEvent) {
        event.preventDefault();
    }

    // 下略

}

一樣也是要檢查有幾個觸控點,外加上 isPanning 這個 instance variable 是否為 true

touch-input.ts

class TouchInput {
    
    // 上略

    touchmoveHandler(event: TouchEvent) {
        event.preventDefault();
        if(this.isPanning && event.targetTouches.length == 1){
            
        }
    }

    // 下略

}

接下來我們需要計算手指現在的位置跟平移起始點的差距。

記得要把向量相減計算的 function import 進來。

touch-input.ts

import { Point, vectorSubtraction } from "./vector";

class TouchInput {
    
    // 上略

    touchmoveHandler(event: TouchEvent) {
        event.preventDefault();
        if(this.isPanning && event.targetTouches.length == 1){
            const curTouchPoint = {x: event.targetTouches[0].clientX, y: event.targetTouches[0].clientY};
            const diff = vectorSubtraction(this.panStartPoint, curTouchPoint);
        }
    }

    // 下略

}

然後把這個差距進行座標系的轉換,這時我們也要仰賴 Camera 這個類別裡面的 transformVector2WorldSpace

所以我們也需要一個 cameraTouchInput 裡面。

記得要從 camera.ts import Camera

touch-input.ts

import { Camera } from "./camera";

class TouchInput {

    // 略

    private camera: Camera;

    // 略

    constructor(camera: Camera) {
        this.camera = camera;
        this.panStartPoint = {x: 0, y: 0};
        this.isPanning = false;
    }

    //略
}

然後我們可以在 touchmoveHandler 裡面去做座標系轉換。

touch-input.ts

class TouchInput {
    
    // 上略

    touchmoveHandler(event: TouchEvent) {
        event.preventDefault();
        if(this.isPanning && event.targetTouches.length == 1){
            const curTouchPoint = {x: event.targetTouches[0].clientX, y: event.targetTouches[0].clientY};
            const diff = vectorSubtraction(this.panStartPoint, curTouchPoint);
            const diffInWorld = this.camera.transformVector2WorldSpace(diff);
        }
    }

    // 下略

}

接下來跟 KeyboardMouseInput 也都一樣,我們需要移動相機然後紀錄新的平移起始點。

touch-input.ts

class TouchInput {
    
    // 上略

    touchmoveHandler(event: TouchEvent) {
        event.preventDefault();
        if(this.isPanning && event.targetTouches.length == 1){
            const curTouchPoint = {x: event.targetTouches[0].clientX, y: event.targetTouches[0].clientY};
            const diff = vectorSubtraction(this.panStartPoint, curTouchPoint);
            const diffInWorld = this.camera.transformVector2WorldSpace(diff);
            this.camera.setPositionBy(diffInWorld);
            this.panStartPoint = curTouchPoint;
        }
    }

    // 下略

}

剩下的就是當手指離開螢幕後,我們需要取消平移模式。

這裡有兩個 event 是我們需要注意的:touchcancel 以及 touchend

我這邊不會詳細解釋這兩個 event 有什麼差別,如果有興趣的讀者可以延伸閱讀。

touchcancel MDN
touchend MDN

我們在 TouchInput 裡面加上這兩個 event 的 Handler。

touch-input.ts

class TouchInput {
    // 上略

    touchendHandler(event: TouchEvent) {

    }

    touchcancelHandler(event: TouchEvent) {

    }

    // 下略
}

這兩個 Handler 我們只需要做把 isPanning 設為 false 即可。

因為我們是手指處碰到螢幕就開始平移了,這個可能不一定是最理想的狀態,有可能你會想要有一個按鈕,按下去才會是平移模式。

那你只需要在外面再加一層用按鈕 toggle 的 flag 然後那個 flag 是 true 的狀態的時候才有後面這些東西就好。

在這個系列裡我會先以目前這樣的操作邏輯為主,如果你有需要我補充示範如何把切換平移模式的邏輯移到別的地方的話再跟我說。我再另外補充。

touch-input.ts

class TouchInput {
    // 上略

    touchendHandler(event: TouchEvent) {
        this.isPanning = false;
    }

    touchcancelHandler(event: TouchEvent) {
        this.isPanning = false;
    }

    // 下略
}

到這邊觸控的平移大致上就完成了!

不過還有一些些東西需要處理主要就是 bind 跟 export 這個 TouchInput 類別。

touch-input.ts

class TouchInput {
    // 上略

    constructor(camera: Camera) {
        this.camera = camera;
        this.panStartPoint = {x: 0, y: 0};
        this.isPanning = false;
        this.touchstartHandler = this.touchstartHandler.bind(this);
        this.touchmoveHandler = this.touchmoveHandler.bind(this);
        this.touchendHandler = this.touchendHandler.bind(this);
        this.touchcancelHandler = this.touchcancelHandler.bind(this);
    }

    // 下略
}

touch-input.ts

export { TouchInput };

接下來是要在 main.ts 加入觸控的邏輯。

先來 import TouchInput 以及建立一個實例。

記得要傳 camera 進去 TouchInput 的建構子裡面~

main.ts

import { TouchInput } from "./src/touch-input";

const touchInput = new TouchInput(camera);

然後就可以把 event listener 掛到 canvas 上了。

main.ts

canvas.addEventListener("touchstart", touchInput.touchstartHandler);
canvas.addEventListener("touchmove", touchInput.touchmoveHandler);
canvas.addEventListener("touchend", touchInput.touchendHandler);
canvas.addEventListener("touchcancel", touchInput.touchcancelHandler);

另外因為沒有 touchleave 這種 event 所以觸控的手指移開 canvas 還是會延續觸控模式,如果要處理這部分的邏輯需要自己再另外想。我這邊就先留白,因為目前我還沒有這個需求,如果哪天我實作出來再來跟大家更新。

這樣就完成平移的所有操作邏輯了。

恭喜你!這算是這個系列的一個里程碑!

接下來我會示範一些平移相關的進階實作,主要是限制相機可以平移到的目的地以及限制的模式。

啊對了,如果你想要測試觸控的平移你可以用你的手機或是平板,跟你的電腦連到同一個 wifi 然後再看看你電腦的 IP 位置是多少,通常會很像是 "192.168.0.110" 之類的一串數字。

應該可以去電腦的設定 -> wifi 然後看你現在連到的網路的細節裡面可以找到 IP。

然後在跑 vite 的時候可以給他 --host 這個 flag 給它,或是在 vite.config.js 裡面給 server.host 設定 true 或是 0.0.0.0 詳細說明

假設 IP 真的是 "192.168.0.110",用你的手機或平板的瀏覽器在網址欄打下 "192.168.0.110:5173"。(記得是要用你的 port 不一定是 5173)

不過這邊有一個坑,就是如果是你用 iPhone 的話,因為 webkit 對 context.reset 的實作跟 chrome 還有 firefox 不太一樣,它並不會清空畫布,所以用手機可能會看起來怪怪的。

這個之後的實作會處理到,我們會用 clearRect 來清空畫布。

今天的進度在這裡

那我們明天見!


上一篇
Day 10 | Pan 平底鍋?
下一篇
Day 12 | 鉗制平移
系列文
無限...寶石?畫布啦!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言