將 LEGO® 積木帶入跨裝置網頁
Build with Chrome 是針對 Chrome 電腦版使用者推出的有趣實驗,最初在澳洲推出,2014 年重新推出後,已在全球推出,並與《LEGO® MOVIE™》合作,且新增支援行動裝置。本文將分享我們從這項專案中學到的幾點,特別是從僅限於電腦的體驗,轉移到同時支援滑鼠和觸控輸入的多螢幕解決方案。
使用 Chrome 進行建構的歷史
第一版「用 Chrome 建構」計畫於 2012 年在澳洲推出。我們希望以全新方式展示網路的強大功能,並讓更多使用者認識 Chrome。
這個網站有兩個主要部分:「建構」模式可讓使用者使用 LEGO 積木建構創作,而「探索」模式則可在 LEGO 版 Google 地圖上瀏覽創作。
互動式 3D 是提供最佳 LEGO 建構體驗的關鍵。2012 年,WebGL 僅在電腦版瀏覽器中公開提供,因此 Build 的目標是提供僅限電腦的體驗。Explore 會使用 Google 地圖顯示創作內容,但在放大到一定程度時,就會切換至 WebGL 實作地圖,以 3D 方式顯示創作內容,同時仍使用 Google 地圖做為底板紋理。我們希望打造一個環境,讓各年齡層的樂高迷都能輕鬆、直覺地發揮創意,並探索彼此的創作。
2013 年,我們決定將「使用 Chrome 建構」擴展至新網路技術。其中包括 Android 版 Chrome 中的 WebGL,這項技術自然可讓 Build with Chrome 進化為行動裝置體驗。首先,我們先開發了觸控原型,然後再針對「建構工具」的硬體進行測試,瞭解瀏覽器與行動應用程式可能會面臨的手勢行為和觸覺回應。
回應式前端
我們需要支援同時支援觸控和滑鼠輸入的裝置。不過,由於空間受限,在小型觸控螢幕上使用相同的 UI 並非最佳解決方案。
在 Build 中,有許多互動功能,包括縮放、變更積木顏色,以及選取、旋轉和放置積木。使用者經常花費大量時間使用這項工具,因此他們必須能快速存取所有常用項目,並且能輕鬆與這項工具互動。
設計互動性極高的觸控應用程式時,您會發現螢幕很快就會變小,而且使用者在互動時,手指往往會遮住大部分螢幕。我們在使用建構工具時就發現了這個問題。進行設計時,請務必考量實體螢幕大小,而非圖像中的像素。因此,請盡量減少按鈕和控制項的數量,讓螢幕空間能盡可能用於顯示實際內容。
我們的目標是讓 Build 在觸控裝置上使用起來更自然,不僅在原始桌面實作中加入觸控輸入,還要讓使用者感覺到 Build 是專為觸控裝置設計。我們最終設計了兩種 UI,一種適用於螢幕較大的電腦和平板電腦,另一種則適用於螢幕較小的行動裝置。盡可能使用單一實作項目,並在模式之間進行流暢的轉換。在我們的案例中,我們判斷這兩種模式的使用體驗有顯著差異,因此決定採用特定的斷點。這兩個版本有許多共同的功能,我們也嘗試只透過單一程式碼實作大部分功能,但兩個版本的 UI 在某些方面運作方式不同。
我們會使用使用者代理程式資料偵測行動裝置,然後檢查檢視區大小,決定是否應使用小螢幕行動 UI。很難選擇「大螢幕」應有的中斷點,因為很難取得可靠的實體螢幕大小值。幸運的是,在我們的案例中,即使在螢幕較大的觸控裝置上顯示小螢幕 UI,工具仍可正常運作,只是部分按鈕可能會顯得過大。最後,我們將中斷點設為 1000 像素;如果您從寬度超過 1000 像素的視窗 (橫向模式) 載入網站,就會看到大螢幕版本。
讓我們來談談這兩種螢幕尺寸和體驗:
大螢幕,支援滑鼠和觸控操作
大螢幕版會提供給所有支援滑鼠的桌上型電腦,以及大螢幕觸控裝置 (例如 Google Nexus 10)。這個版本的導覽控制項類型與原始電腦版解決方案相近,但我們新增了觸控支援和部分手勢。我們會根據視窗大小調整 UI,因此當使用者調整視窗大小時,系統可能會移除或調整部分 UI 的大小。我們會使用 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 的情況下複製「建構和探索」3D 功能。
為求一致性,所有平台的創作內容視覺風格必須一致。我們可以嘗試使用 2.5D 解決方案,但這會讓創作內容在某些方面看起來有所不同。我們也必須考量如何確保使用第一版「建構與 Chrome 應用程式」建立的作品,在舊版網站和新版網站中都能以相同方式顯示,並順利執行。
即使您無法在 3D 模式下建立新創作或探索,非 WebGL 裝置仍可使用探索 2D 模式。因此,即使使用者使用的是支援 WebGL 的裝置,也能瞭解專案的深度,以及他們可以使用這項工具製作的內容。對於不支援 WebGL 的使用者來說,這個網站可能不太實用,但至少可以做為預告片,吸引他們試用。
有時無法保留 WebGL 解決方案的備用版本。這可能是因為效能、視覺風格、開發和維護成本等因素。不過,如果您決定不實作備用方案,至少應處理不支援 WebGL 的訪客,說明他們無法完全存取網站的原因,並提供使用支援 WebGL 的瀏覽器解決問題的指示。
資產管理
2013 年,Google 推出新版 Google 地圖,並在推出後首次大幅調整使用者介面。因此,我們決定重新設計「使用 Chrome 建構」功能,以便與新版 Google 地圖 UI 相容,並在重新設計時考量其他因素。新設計採用相對平面、簡單的純色和簡單形狀。這讓我們可以在許多 UI 元素上使用純 CSS,盡量減少圖片的使用。
在「探索」中,我們需要載入許多圖片,包括創作內容的縮圖、地圖底板的紋理,以及實際的 3D 創作內容。我們會特別小心,確保在持續載入新圖片時不會發生記憶體流失。
3D 創作會以 PNG 圖片封裝成自訂檔案格式儲存。將 3D 創作資料儲存為圖片,可讓我們將資料直接傳遞至算繪創作的著色器。
對於所有使用者提供的圖片,我們設計了相同的圖片大小,可在所有平台上使用,因此可盡量減少儲存空間和頻寬的使用量。
管理螢幕方向
從直向模式切換至橫向模式,或從橫向模式切換至直向模式時,很容易忘記螢幕顯示比例的變化幅度。因此,在為行動裝置調整時,請一開始就考慮這點。
在啟用捲動的傳統網站上,您可以套用 CSS 規則,讓回應式網站重新排列內容和選單。只要您可以使用捲動功能,這項功能就相當好管理。
我們也使用了這個方法來建構,但在解決版面配置時受到了一些限制,因為我們需要讓內容隨時可見,同時還要能快速存取多個控制項和按鈕。對於新聞網站等純內容網站而言,流動式版面配置非常實用,但對於像我們這種遊戲應用程式而言,就比較難以實現。要找到在橫向和直向模式下皆可運作的版面配置,同時還能讓使用者清楚掌握內容,並提供舒適的互動方式,是一項挑戰。最後,我們決定只保留「僅在橫向模式下建構」選項,並告知使用者旋轉裝置。
無論是橫向還是直向,探索模式都更容易解決。我們只需要根據方向調整 3D 的縮放等級,即可獲得一致的體驗。
大部分的內容版面配置都由 CSS 控管,但某些與方向相關的內容則需要以 JavaScript 實作。我們發現,目前沒有任何跨裝置的解決方案可使用 window.orientation 來識別方向,因此最後我們只比較 window.innerWidth 和 window.innerHeight 來識別裝置的方向。
if( window.innerWidth > window.innerHeight ){
//landscape
} else {
//portrait
}
新增觸控支援
為網頁內容新增觸控支援功能相當簡單。基本互動功能 (例如點擊事件) 在電腦和支援觸控的裝置上運作方式相同,但如果是更進階的互動功能,您也需要處理觸控事件:touchstart、touchmove 和 touchend。本文將說明如何使用這些事件的基本概念。Internet Explorer 不支援觸控事件,而是使用指標事件 (pointerdown、pointermove、pointerup)。指標事件已提交至 W3C 進行標準化,但目前僅在 Internet Explorer 中實作。
在探索 3D 模式中,我們希望提供與標準 Google 地圖相同的導覽功能,也就是使用單指平移地圖,以及雙指撥動縮放。由於創作內容是 3D 模型,我們也新增了雙指旋轉手勢。這通常需要使用觸控事件。
建議您避免進行大量運算,例如在事件處理常式中更新或算繪 3D 內容。請改為將觸控輸入內容儲存在變數中,並在 requestAnimationFrame 算繪迴圈中對輸入內容做出反應。這樣一來,您也可以更輕鬆地同時實作滑鼠,只要將對應的滑鼠值儲存在相同的變數中即可。
首先,請初始化物件來儲存輸入內容,並新增 touchstart 事件監聽器。在每個事件處理常式中,我們都會呼叫 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;
}
}
當您使用觸控移動事件製作動畫時,通常也需要儲存上次事件後的差異移動量。舉例來說,我們將這項參數用於 Explore 中所有底座板的移動速度,因為你並非拖曳底座板,而是實際移動攝影機。
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
多點觸控手勢
Hammer 或 QuoJS 等多個架構或程式庫可簡化多點觸控手勢的管理作業,但如果您想結合多個手勢並取得完整控制權,有時最好還是從頭開始。
為了管理捏合和旋轉手勢,我們會儲存第二根手指放置在螢幕上時,兩根手指之間的距離和角度:
//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;
}
}
您可以使用與拖曳範例類似的方式,利用各個 touchmove 事件之間的距離變化,但這種做法通常在您想要持續移動時較為實用。
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()
,系統也會觸發一些模擬的滑鼠事件,以便讓大多數非觸控最佳化網站繼續運作。舉例來說,如果單手在螢幕上輕觸一次,系統可能會依照以下順序快速觸發這些事件:
- touchstart
- touchmove
- touchend
- 滑鼠游標懸停
- mousemove
- mousedown
- mouseup
- click
如果互動行為較為複雜,這些滑鼠事件可能會導致一些非預期的行為,並破壞您的實作方式。通常建議在觸控事件處理常式中使用 event.preventDefault()
,並在個別事件處理常式中管理滑鼠輸入內容。請注意,在觸控事件處理常式中使用 event.preventDefault()
也會阻止某些預設行為,例如捲動和點擊事件。
「在『Build with Chrome』中,我們不希望在使用者雙擊網站時發生縮放動作,即使這在大多數瀏覽器中是標準功能。因此,我們使用可視區域中繼標記,告知瀏覽器在使用者雙擊時不要縮放。這麼做也可以移除 300 毫秒的點擊延遲時間,進而改善網站的回應速度。(點按延遲時間可在啟用輕觸兩下縮放功能時,區分單指輕觸和雙指輕觸)。
<meta name="viewport" content="width=device-width,user-scalable=no">
請注意,使用這項功能時,您必須確保網站可以在所有螢幕尺寸下正常顯示,因為使用者無法再放大畫面。
滑鼠、觸控和鍵盤輸入
在探索 3D 模式中,我們希望提供三種導覽地圖的方式:滑鼠 (拖曳)、觸控 (拖曳、雙指撥動和旋轉) 和鍵盤 (以方向鍵導覽)。所有這些導覽方法的運作方式略有不同,但我們在所有方法上都採用相同的做法,也就是在事件處理常式中設定變數,並在 requestAnimationFrame 迴圈中對其採取行動。requestAnimationFrame 迴圈不需要知道用來導覽的方法。
舉例來說,我們可以使用所有三種輸入方法設定地圖的移動方式 (dragDX 和 dragDY)。以下是鍵盤實作方式:
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。
接著,如果您尚未建構出色的內容,現在就開始吧!