透過 Chrome 建構內容

為多裝置網站提供 LEGO® 積木

Hans Eklund
Hans Eklund

Build with Chrome 是最初在澳洲推出的電腦版 Chrome 使用者有趣的實驗功能,我們在 2014 年重新推出,包括在全球推出、與 LEGO® MovieTM 密切合作,並新增行動裝置支援。在本文中,我們會分享從這項專案中學到的經驗,特別是從僅限電腦體驗改為同時支援滑鼠和觸控輸入的多螢幕解決方案。

Build with Chrome 的歷史

第一版 Build with Chrome 是在 2012 年在澳洲推出。我們想以全新的方式展示網路的強大功能,並將 Chrome 引進全新的目標對象。

這個網站主要分為兩大部分:「建設」模式 (使用者可使用 LEGO 積木建造),以及「探索」模式,可在 LEGO 化版的 Google 地圖上瀏覽作品。

互動式 3D 是為使用者提供最佳的樂高建築體驗。2012 年,WebGL 僅透過電腦版瀏覽器公開使用,因此 Build 是專為電腦版所設計。「探索」使用 Google 地圖顯示創作內容,但放大到最大程度時,地圖會切換為 WebGL 實作,以顯示 3D 形式的地圖內容,目前仍使用 Google 地圖做為底板紋理。我們希望打造一個環境,讓所有年齡層的 LEGO 愛好者輕鬆、直覺地表達創意及探索彼此的作品。

2013 年,我們決定將 Build with Chrome 納入新網路技術中。在 Android 版 Google Chrome 中,WebGL 便是 WebGL,使得 Build with Chrome 可以進化為行動裝置體驗。首先,我們首先開發觸控原型,然後在詢問「建構工具」的硬體之前,瞭解可能透過瀏覽器處理的手勢行為和觸覺回應,並與行動應用程式比較。

回應式前端

因此,我們必須支援支援觸控和滑鼠輸入的裝置。然而,由於空間限制,在小型觸控螢幕上使用相同 UI 已選擇不太理想的解決方案。

Build 中有許多互動功能,例如放大和縮小、變更磚塊,以及選擇、旋轉和放置磚塊。使用者經常使用這項工具,因此他們必須能快速存取經常使用的所有功能,而且使用者應能放心使用這項工具。

設計互動性高的觸控應用程式時,您會注意到螢幕很快就感覺很小,而且使用者的手指往往在互動過程中會覆蓋許多螢幕。在使用建構工具時,這一點顯而易見。設計廣告素材時,必須考量實體螢幕大小,而非圖片中的像素。務必減少按鈕和控制項的數量,盡可能讓整個畫面空間都放在實際內容上。

我們的目標是讓 Build 在觸控裝置上自然流暢,不只在原本實作的桌面中加入觸控輸入,也希望營造出真實的觸控體驗。我們最後打造出兩種 UI 版本:一種適用於配備大螢幕的電腦和平板電腦,另一種則適用於小型螢幕的行動裝置。請盡可能使用單一實作,並在模式之間流暢轉換。在這個案例中,我們確定這兩種模式之間的體驗有明顯的差異,因此我們決定仰賴特定的中斷點。這兩個版本包含許多共通功能,我們嘗試僅使用單一程式碼來執行大部分作業,但兩者的使用者介面某些部分在運作時不同。

我們會使用使用者代理程式資料偵測行動裝置,然後檢查可視區域的大小,藉此判斷是否應使用小型螢幕的行動裝置使用者介面。為「大螢幕」選擇中斷點並不容易,因為要取得可靠的實體螢幕大小值並不容易。幸好,以本例來說,在具有大螢幕的觸控裝置上顯示小螢幕 UI 並不重要,因為此工具仍可正常運作,只是有些按鈕可能會稍微太大。最後,我們會將中斷點設為 1000 像素;如果您是從寬度超過 1000 像素的視窗載入網站 (橫向模式),就會看到大螢幕版本。

讓我們簡單說明兩種螢幕大小和體驗:

支援滑鼠和觸控功能的大螢幕

大螢幕版本適用於所有支援滑鼠的桌上型電腦,以及具有大螢幕的觸控裝置 (例如 Google Nexus 10)。這個版本與最初的桌面解決方案類似,可運用各種瀏覽控制項,但我們加入了觸控支援和一些手勢。我們會根據視窗大小調整使用者介面,因此當使用者調整視窗大小時,系統可能會移除或調整部分使用者介面的大小。方法是使用 CSS 媒體查詢

範例:如果可用的高度小於 730 像素,探索模式中的縮放滑桿控制項會隱藏:

@media only screen and (max-height: 730px) {
    .zoom-slider {
        display: none;
    }
}

僅支援小螢幕,僅支援觸控

這個版本適用於行動裝置和小型平板電腦 (鎖定 Nexus 4 和 Nexus 7 裝置)。這個版本需要多點觸控支援。

在小螢幕裝置上,我們需要盡可能讓內容預留最大的螢幕空間,因此針對空間進行了一些調整,以便盡可能利用空間,而將不常使用的元素移到視覺上。

  • 建構積木選擇工具可在建築物時最小化為色彩選取器。
  • 我們用多點觸控手勢取代縮放和方向控制項。
  • Chrome 的全螢幕功能也有助於取得更多螢幕空間。
在大型螢幕上進行建構
在大螢幕上進行建構。板塊選擇器會持續顯示,右邊還有一些控制選項。
在小螢幕上進行建構
用小螢幕進行建構。強制選擇工具已最小化,且部分按鈕已移除。

WebGL 效能與支援

新型觸控裝置配備強大的 GPU,但距離電腦較遠,因此我們瞭解效能可能會遇到一些挑戰,尤其是在探索 3D 模式下,我們需要同時轉譯大量作品。

在創意上,我們想添加幾種新類型的磚,不僅外型複雜,而且甚至透明度,這些特點在 GPU 上通常相當繁重。然而,我們必須具有回溯相容性,並繼續支援第一個版本建立的內容,因此無法設定任何新的限制,例如大幅減少製作過程中的積木總數。

在第一個版本的 Build 中,可用於一次建立的積木數量已達上限。有幾塊「磚尺」代表剩下的積木數量。在新的實作項目中,我們採用一些新積木,相較於標準積木,對積木儀造成比標準積木更多影響,因此會略微降低積木總數。這個方法可以在加入新積木的同時,兼顧效能。

「探索 3D 模式」同時執行許多工作,例如載入底板紋理、載入作品、製作動畫和算繪內容等。這是因為 GPU 和 CPU 需要大量資源,因此我們在 Chrome 開發人員工具中針對這些部分進行了大量的影格剖析,盡可能最佳化相關部分。在行動裝置上,我們決定將圖片放大一點,不必同時轉譯太多創作。

有些裝置重新審視並簡化部分 WebGL 著色器,但我們一直找到解決方法,希望日後能繼續推動。

支援非 WebGL 裝置

我們希望即使訪客的裝置不支援 WebGL,也能有可用的網站。有時候,您或許可以使用畫布解決方案或 CSS3D 功能,以簡化的方式呈現 3D 圖像。很遺憾,我們目前沒有足夠的解決方案,無法在不使用 WebGL 的情況下複製 Build 與探索 3D 功能。

為保持一致性,創作的視覺風格必須在所有平台上相同。我們也許已試過 2.5D 解決方案,但這使得創作看起來不太一樣。我們也必須考量如何確保

非 WebGL 裝置的使用者仍可使用探索 2D 模式,即使無法建構新創作,也無法以 3D 模式探索。這樣一來,使用者即使使用支援 WebGL 的裝置,仍然可以瞭解專案的深度,以及透過這項工具創造什麼內容。不支援 WebGL 對使用者而言,網站價值可能較低,但至少應作為前導廣告,讓使用者能參與試用。

有時可能無法保留 WebGL 解決方案的備用版本。導致效能、視覺風格、開發和維護費用等因素有很多。不過,如果您決定不導入備用選項,則至少應提供不支援 WebGL 功能的訪客,說明他們無法完全存取網站的原因,並指示他們使用支援 WebGL 以解決問題的方法。

資產管理

Google 在 2013 年推出的新版 Google 地圖,自推出以來最重大的使用者介面變更。因此,我們決定根據新版 Google 地圖使用者介面,重新設計 Build with Chrome,配合新版 Google 地圖使用者介面,同時將其他因素納入考量。新設計相對平坦,並且採用簡潔的單色和簡單形狀。這讓我們能夠在許多 UI 元素中使用純 CSS,盡可能減少圖片的使用。

「探索」工具需要載入大量圖片、創作縮圖、繪製底板的紋理,最後再呈現實際的 3D 作品。我們採取額外措施,是為了確保持續載入新圖片時,不會發生記憶體流失。

3D 作品會以 PNG 圖片封裝的自訂檔案格式儲存。保留以圖片形式儲存的 3D 建立資料,可讓我們基本上將資料直接傳遞給轉譯器的著色器。

透過這項設計,我們可以在所有平台中使用相同的圖片大小,以減少儲存空間和頻寬使用量。

管理螢幕方向

從直向切換為橫向模式或其他方向時,你很容易忘記螢幕顯示比例的變化。因此,在為行動裝置調整內容時,必須考量這點。

在啟用捲動功能的傳統網站上,您可以套用 CSS 規則,取得重新排列內容和選單的回應式網站。只要您可以使用捲動功能,就能輕鬆管理。

我們也將這個方法與 Build 搭配使用,但我們在解決版面配置方面有一些限制,因為我們需要讓內容隨時顯示,並能快速存取許多控制項和按鈕。對單純的內容網站 (例如新聞網站) 而言,採用流暢的版面配置並不可靠,但對像我們這樣的遊戲應用程式來說,這可不是件容易的事。想找出適合橫向和直向的版面配置,同時清楚大致瞭解內容及提供舒適的互動方式,這項挑戰成為一大挑戰。最後,我們決定只使用橫向版本,並通知使用者旋轉裝置。

無論使用何種螢幕方向,探索功能都比問題容易多了。我們只需要依照方向調整 3D 的縮放等級,以獲得一致的體驗。

大部分內容版面配置都是由 CSS 控制,但必須在 JavaScript 中實作一些與方向相關的內容。我們發現使用 window.orientation 判斷方向的跨裝置解決方案並不理想,因此在最後,我們僅比較 window.innerWidth 和 window.innerHeight 來確定裝置的方向。

if( window.innerWidth > window.innerHeight ){
  //landscape
} else {
  //portrait
}

新增觸控支援

在網路內容中新增觸控支援功能相當簡單。在電腦和支援觸控的裝置上,基本互動功能 (例如點擊事件) 的運作方式並無不同,但若要執行更進階的互動,您也需要處理觸控事件:觸控開始、觸控移動和觸控。本文將介紹這些事件的基本使用方式。Internet Explorer 不支援觸控事件,但會使用遊標事件 (指標、指標移動、指標)。指標事件已提交至 W3C 進行標準化,但目前只能在 Internet Explorer 中實作。

在「探索 3D」模式中,我們需要的導覽方式與標準 Google 地圖實作相同,使用單指平移地圖和雙指撥動縮放。由於創作是以 3D 呈現,因此我們新增了雙指旋轉手勢。這通常是需要使用觸控事件。

建議您避免使用大量運算,例如在事件處理常式中更新或轉譯 3D。請改為將觸控輸入儲存在變數中,然後在 requestAnimationFrame 轉譯迴圈中的輸入內容做出回應。如此也能更輕鬆同時實作滑鼠,只要將對應的滑鼠值儲存在相同的變數中即可。

首先,請初始化物件來儲存輸入內容,然後新增觸控啟動事件監聽器。在每個事件處理常式中,我們會呼叫 event.preventDefault()。這是為了避免瀏覽器繼續處理觸控事件,進而引發一些非預期的行為,例如捲動或縮放整個網頁。

var input = {dragStartX:0, dragStartY:0, dragX:0, dragY:0, dragDX:0, dragDY:0, dragging:false};
plateContainer.addEventListener('touchstart', onTouchStart);

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
    //start listening to all needed touchevents to implement the dragging
    document.addEventListener('touchmove', onTouchMove);
    document.addEventListener('touchend', onTouchEnd);
    document.addEventListener('touchcancel', onTouchEnd);
  }
}

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }
}

function onTouchEnd(event) {
  event.preventDefault();
  if( event.touches.length === 0){
    handleDragStop();
    //remove all eventlisteners but touchstart to minimize number of eventlisteners
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
    //also listen to touchcancel event to avoid unexpected behavior when switching tabs and some other situations
    document.removeEventListener('touchcancel', onTouchEnd);
  }
}

我們不會在事件處理常式中實際儲存輸入資料,而是使用不同的處理常式,例如 handleDragStart、HandleDragging 和 handleDragStop。這是因為我們也想要從滑鼠事件處理常式呼叫這些函式。提醒您,雖然少見可能,但使用者可以同時使用觸控和滑鼠。我們不會直接處理該案件,只會確保沒有任何東西發生。

function handleDragStart(x ,y ){
  input.dragging = true;
  input.dragStartX = input.dragX = x;
  input.dragStartY = input.dragY = y;
}

function handleDragging(x ,y ){
  if(input.dragging) {
    input.dragDX = x - input.dragX;
    input.dragDY = y - input.dragY;
    input.dragX = x;
    input.dragY = y;
  }
}

function handleDragStop(){
  if(input.dragging) {
    input.dragging = false;
    input.dragDX = 0;
    input.dragDY = 0;
  }
}

根據觸控移動執行動畫時,請一併儲存上次事件後的差異遷移。舉例來說,在「探索」中移動所有底板時,我們使用這項參數做為相機速度的參數,因為您實際上並未拖曳底板,而是移動攝影機。

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );

  //execute animation based on input.dragDX, input.dragDY, input.dragX or input.dragY
 /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

內嵌範例:使用觸控事件拖曳物件。在 Build with Chrome 中拖曳「探索」3D 地圖的方式類似:http://cdpn.io/qDxvo

多點觸控手勢

有數種架構或程式庫 (例如 HammerQuoJS) 可協助您簡化多點觸控手勢的管理作業,但如果您想結合多種手勢並取得完整控制權,建議您從頭開始。

為了管理雙指撥動和旋轉手勢,我們會儲存第二指輕觸螢幕的距離和角度:

//variables representing the actual scale/rotation of the object we are affecting
var currentScale = 1;
var currentRotation = 0;

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGestureStart(x1, y1, x2, y2){
  input.isGesture = true;
  //calculate distance and angle between fingers
  var dx = x2 - x1;
  var dy = y2 - y1;
  input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
  input.touchStartAngle=Math.atan2(dy,dx);
  //we also store the current scale and rotation of the actual object we are affecting. This is needed to support incremental rotation/scaling. We can't assume that an object is always the same scale when gesture starts.
  input.startScale=currentScale;
  input.startAngle=currentRotation;
}

在觸控移動事件中,我們會持續測量雙指之間的距離和角度。起點和目前距離的差之後會用來設定體重計,而開始角度和目前角度的差則用來設定角度。

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length  === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGesture(x1, y1, x2, y2){
  if(input.isGesture){
    //calculate distance and angle between fingers
    var dx = x2 - x1;
    var dy = y2 - y1;
    var touchDistance = Math.sqrt(dx*dx+dy*dy);
    var touchAngle = Math.atan2(dy,dx);
    //calculate the difference between current touch values and the start values
    var scalePixelChange = touchDistance - input.touchStartDistance;
    var angleChange = touchAngle - input.touchStartAngle;
    //calculate how much this should affect the actual object
    currentScale = input.startScale + scalePixelChange*0.01;
    currentRotation = input.startAngle+(angleChange*180/Math.PI);
    //upper and lower limit of scaling
    if(currentScale<0.5) currentScale = 0.5;
    if(currentScale>3) currentScale = 3;
  }
}

您可以像使用拖曳範例一樣,使用每個觸控移動事件之間的距離變化,但這個方法通常適合用於連續移動。

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //execute transform based on currentScale and currentRotation
  /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

您也可以啟用物件拖曳功能,只要用雙指撥動或旋轉手勢即可。在這種情況下,您應使用兩根手指之間的中心點做為拖曳處理常式的輸入內容。

內嵌範例:以 2D 模式旋轉物件及調度資源。「探索」中的地圖導入方式類似:http://cdpn.io/izloq

同一個硬體的滑鼠和觸控支援

現今,有多台筆電 (例如 Chromebook Pixel) 支援滑鼠和觸控輸入。否則可能會導致某些非預期的行為。

重要的是,您不應單純偵測觸控支援,然後忽略滑鼠輸入,而是同時支援兩者。

如果您在觸控事件處理常式中未使用 event.preventDefault(),系統也會觸發一些模擬的滑鼠事件,以便讓大多數非觸控版網站都能照常運作。舉例來說,如果在畫面上輕觸一下,系統就會以快速順序觸發這些事件,並按照以下順序觸發:

  1. 觸控啟動
  2. 觸控移動
  3. 輕觸
  4. 滑鼠游標懸停
  5. mousemove
  6. 滑鼠下移
  7. 老鼠
  8. 按一下

如果您有更複雜的互動,這些滑鼠事件可能會導致一些非預期的行為,並阻礙您的實作。通常建議您在觸控事件處理常式中使用 event.preventDefault(),並使用個別事件處理常式管理滑鼠輸入。您必須瞭解,在觸控事件處理常式中使用 event.preventDefault() 也會阻止某些預設行為,例如捲動和點擊事件。

「在 Build with Chrome 中,我們不希望使用者輕觸兩下網站時會發生縮放情況,即使大多數瀏覽器是標準功能,因此,當使用者輕觸兩下時,我們會利用可視區域中繼標記,告知瀏覽器不要縮放。並消除 300 毫秒的點擊延遲,得以改善網站的回應速度。(在啟用輕觸兩下縮放功能時,點選延遲會產生點擊延遲)。

<meta name="viewport" content="width=device-width,user-scalable=no">

提醒您,使用這項功能時,可以選擇讓網站在所有螢幕大小上都能讀取,因為使用者無法縮小畫面。

滑鼠、觸控和鍵盤輸入

「探索 3D 模式」提供三種瀏覽地圖的方式:滑鼠 (拖曳)、輕觸 (拖曳、雙指撥動縮放及旋轉) 和鍵盤 (使用方向鍵瀏覽)。所有這些導覽方法的運作方式都稍有不同,但我們針對所有這些方法都採用相同的方法;在事件處理常式中設定變數,並在 requestAnimationFrame 迴圈中採取動作。requestAnimationFrame 迴圈不必知道要用哪個方法進行導覽。

舉例來說,我們可以使用這三種輸入法設定地圖的移動 (拖曳和拖曳 YY)。鍵盤實作方式如下:

document.addEventListener('keydown', onKeyDown );
document.addEventListener('keyup', onKeyUp );

function onKeyDown( event ) {
  input.keyCodes[ "k" + event.keyCode ] = true;
  input.shiftKey = event.shiftKey;
}

function onKeyUp( event ) {
  input.keyCodes[ "k" + event.keyCode ] = false;
  input.shiftKey = event.shiftKey;
}

//this needs to be called every frame before animation is executed
function handleKeyInput(){
  if(input.keyCodes.k37){
    input.dragDX = -5; //37 arrow left
  } else if(input.keyCodes.k39){
    input.dragDX = 5; //39 arrow right
  }
  if(input.keyCodes.k38){
    input.dragDY = -5; //38 arrow up
  } else if(input.keyCodes.k40){
    input.dragDY = 5; //40 arrow down
  }
}

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //because keydown events are not fired every frame we need to process the keyboard state first
  handleKeyInput();
  //implement animations based on what is stored in input
   /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX = 0;
  input.dragDY = 0;
}

嵌入範例:使用滑鼠、觸控和鍵盤進行瀏覽:http://cdpn.io/catlf

摘要

將 Build with Chrome 改寫為支援不同螢幕大小的觸控裝置,具有良好的學習體驗。這個團隊在互動裝置上展現過這種互動性的能力並不高,在過程中我們也學到了很多寶貴的經驗。

而其中最大的挑戰就是如何解決使用者體驗和設計問題。管理多種螢幕尺寸、觸控事件和效能問題都是這項技術上的難題。

即使觸控裝置上的 WebGL 著色器有一些挑戰,但這是比預期更好的操作。隨著裝置功能日益強大,WebGL 實作項目也正在迅速改善。我們相信不久後就會在裝置中使用 WebGL。

如果還沒這樣做,現在就開始打造精彩內容吧!