Build with Chrome

Bringing LEGO® bricks to the Multi-Device Web

HTML5 Rocks

데스크탑 크롬 사용자들을 위한 재밌는 실험인 Build with Chrome은 원래 오스트레일리아에서 런칭되었으며, 2014년 THE LEGO® MOVIE™와 맞추어 전세계를 대상으로 서비스가 가능하도록 다시 릴리즈되었고 모바일 디바이스들을 지원하는 기능이 새로 추가되었습니다. 이 글에서 프로젝트로부터 배우게 된 몇가지를 특히 데스크탑 전용의 경험을 마우스와 터치 입력을 지원하는 멀티 스크린으로 이동하는 것에 관하여 공유하고자 합니다.

Build with Chrome의 역사

Build with Chrome의 첫번째 버전은 2012년 오스트레일리아에서 런칭되었습니다. 우리는 완전히 새로운 방식으로 웹의 강력함을 보여주고 크롬을 완전히 새로운 관객들에게 데려가고 싶었습니다.

사이트는 사용자가 LEGO 벽돌들을 사용하여 구조물을 구축할 수 있도록 하는 "빌드(Build)"모드와 구글맵의 LEGO화된 버전 상에서 구조물을 브라우징하기 위한 "탐색(Explorer)" 모드의 2가지 주요 영역을 가지고 있습니다.

인터랙티브 3D는 근본적으로 사용자에게 가장 훌륭한 LEGO 구축 경험을 제공할 수 있습니다. 2012년에는 WebGL이 공식적으로 데스크탑 브라우저에서만 사용할 수 있었으므로 빌드는 데스크탑 전용 경험을 대상으로 삼고 있었습니다. 구조물을 디스플레이하기 위해 구글 맵을 사용하여 탐색했지만 이를 충분히 가깝게 줌(Zoom)할 때 3D로 구조물을 보여주는 맵의 WebGL 구현으로 전환하였고, 여전히 구글 맵은 기초적인 텍스쳐로써 사용되었습니다. 우리는 모든 연령의 LEGO 광신자들이 쉽고 직관적으로 그들의 창조성을 표현하고 각기 다른 사람들의 구조물들을 탐색할 수 있기를 바랬습니다.

2013년 우리는 Build with Chrome을 새로운 웹 기술로 확장하기로 결정했습니다. 안드로이드용 크롬에서 WebGL과 같은 기술들은 자연스럽게 Build with Chrome을 모바일 경험으로 이끌어내도록 하였습니다. 시작을 위해 우리는 "빌더 도구(Builder Tool)"을 위한 하드웨어에 대한 의문 전에 모바일 앱과 비교하여 브라우저에서 직면하게 될 제스쳐 동작과 촉각의 민감성을 이해하기 위해 먼저터치 프로토타입을 개발했습니다.

반응형 프론트엔드

우리는 터치와 마우스 입력을 사용하는 디바이스를 모두 지원하는 것이 필요했습니다. 그러나, 작은 터치 스크린 상에서 동일한 UI를 사용하는 것은 공간 제약으로 인해 차선책으로 판가름이 났습니다.

빌드에서는 줌인과 줌아웃, 벽돌 색상의 변경 그리고 물론 벽돌의 선택, 회전 그리고 놓아두기와 같은 많은 상호작용들이 진행됩니다. 이는 사용자들이 때로 많은 시간을 소요하는 도구이므로 그들이 자주 사용하는 모든 것들을 빠르게 접근하고 이와 상호작용하는 것을 편하게 느끼도록 하는 것이 중요합니다.

고수준의 인터랙티브 터치 어플리케이션을 디자인할 때 스크린이 작다는 것이 빠르게 알게되고 사용자의 손가락은 상호작용을 하는 동안 스크린의 많은 부분을 커버한다는 것을 여러분은 발견하게 될 것입니다. 이는 빌더를 사용하여 작업하는 동안 우리에게 명확하게 다가옵니다. 여러분이 디자인을 하는 동안 그래픽에서의 픽셀들보다는 차라리 물리적인 스크린 사이즈에 대해 정말 고려하여야 할 것입니다. 실제 컨텐츠에 전념할 수 있도록 가능한 많은 실제 스크린 점유 영역을 얻기 위해 버튼과 컨트롤들의 숫자를 최소화하는 것이 중요하게 됩니다.

우리의 목표는 데스크탑의 원본 구현체에 그냥 터치 입력을 추가하는 것이 아니라 빌드를 터치 디바이스에서 자연스럽게 느끼도록 만들고 정말 터치 입력을 의도한 것처럼 느끼도록 하는 것입니다. 우리는 2가지의 UI 변경 버전을 완료했습니다. 하나는 넓은 스크린을 가진 데스크탑과 태블릿을 위한 것이고 다른 하나는 보다 작은 스크린을 가진 모바일 디바이스를 위한 것입니다. 가능한 한 하나의 구현을 사용하고 모드 간에 유동적인 전환을 가지는 것이 최선책입니다. 우리의 경우, 명확한 구획 지점에 따르기로 한 2가지 모드 사이에서 사용자 경험이 명백한 차이가 있을 것이라고 판정했습니다. 2가지 버전은 공통적인 많은 기능을 가지고 있었으며 대부분의 것들을 단지 하나의 코드 구현에서 처리하기 위해 노력했습니다만 몇가지 양상에서 UI는 양자간에 다르게 동작합니다.

우리는 User-agent 데이터를 모바일 디바이스를 검출하기 위해 사용했으며 그후 작은 스크린의 경우 모바일 UI 사용 여부를 결정하기 위해 뷰포트 사이즈를 확인했습니다. 무엇이 "큰 스크린"인지를 결정하기 위한 구획지점을 고르는 것은 꽤 어렵습니다. 왜냐하면 물리적인 스크린 크기의 신뢰할만한 값을 얻는 것이 힘들기 때문입니다. 다행스럽게도 우리의 경우 도구는 여전히 잘 동작했으며 단지 몇가지 버튼만이 약간 더 크게 느껴졌을 뿐이라 큰 스크린을 가진 터치 디바이스 상의 작은 화면 UI를 표시할지를 정말로 고려하지 않았습니다. 최종적으로 우리는 구획지점을 1000 픽셀로 정의했습니다. 만약 여러분이 (Landscape 모드에서) 1000 픽셀보다 넓은 윈도우에서 사이트를 로딩한다면 큰 스크린 버전을 볼 수 있을 것입니다.

2가지 화면 크기와 경험들에 대해 좀 더 얘기를 해보도록 하죠.

마우스와 터치를 지원하는 큰 화면

큰 화면 버전은 마우스를 지원하는 모든 데스크탑 컴퓨터와 (구글 넥서스 10같은) 큰 화면을 가진 터치 디바이스에 제공됩니다. 이 버전은 탐색 컨트롤과 같은 것들이 가능하지만 터치 지원과 몇가지 제스쳐가 추가된 오리지널 데스크탑 솔루션에 가깝습니다. 윈도우 크기에 의존하는 UI를 조정하였으므로 사용자가 윈도우를 리사이즈할 때 몇가지 UI가 제거되거나 리사이즈될 것입니다. 우리는 CSS media queries를 사용하여 이를 구현했습니다.

예시: 높이가 730 픽셀보다 작을 때 탐색 모드에서 줌-슬라이더 컨트롤이 다음과 같이 감춰집니다.

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

터치만 지원하는 작은 화면

이 버전은 모바일 디바이스들과 작은 태블릿(넥서스 4와 넥서스 7 같은 대상 디바이스들)에 제공됩니다. 이 버전은 멀티-터치 지원을 요구합니다.

작은 화면을 가진 디바이스 상에서 가능한 한 많은 화면 실 점유를 통해 컨텐츠를 제공할 필요가 있었으므로 공간을 최대화하기 위해 몇가지 조정을 했으며 대다수는 다음과 같이 잘 사용하지 않는 요소들을 화면 밖으로 이동하는 것이었습니다.

  • 빌딩을 하는 동안 빌드의 벽돌 선택기(Brick chooser)는 색상 선택기(Color selector) 내로 최소화됩니다.
  • 줌과 방향 컨트롤을 멀티 터치 제스쳐로 대치했습니다.
  • 크롬 풀스크린 기능 또한 화면에 대한 어느 정도의 추가적인 점유에 도움을 줍니다.
Build on a large screen
큰 화면에서의 빌드. 벽돌 선택기는 항상 나타나 있으며 몇가지 컨트롤들이 오른편에 존재합니다.
Build on a small screen
작은 화면에서의 빌드. 벽돌 선택기는 최소화되어 있으며 몇가지 버튼이 제거되어 있습니다.

WebGL 성능과 지원

현재의 터치 디바이스들은 꽤 강력한 GPU들을 가지고 있지만 여전히 데스크탑의 상대가 되기는 어려우므로 성능, 특히 동시간에 많은 구조물을 렌더링할 필요가 있는 3D 탐색 모드에 대해 몇가지 도전과제가 있음을 인식하고 있었습니다.

창조적으로, 우리는 -일반적으로 GPU 상에서 매우 무거운 기능인- 복잡한 모양과 심지어 투명도까지 가지는 새로운 타입의 벽돌을 몇개 추가하기를 원했습니다. 그렇지만 하위 호환성을 유지해야 했으며 첫버전에서의 구조물에 대한 지원을 지속해야 했으므로 구조물에서 벽돌의 총 갯수를 명시적으로 감소시키는 것과 같은 새로운 제한을 설정할 수는 없었습니다.

빌드의 첫번째 버전은 하나의 구조물에서 사용될 수 있는 벽돌의 최대 제한을 가지고 있었습니다. 여기에는 얼마나 많은 벽돌이 남아 있는지를 알려주는 "벽돌-계량기(Brick-meter)"가 있었습니다. 새로운 구현에서 새로운 벽돌들 중 몇가지는 기본적인 별돌들에 비해 벽돌-계량기에 더 많은 영향을 주었으므로 전체 벽돌 갯수가 약간 감소되었습니다. 이는 여전히 괜찮은 성능을 유지하면서 새로운 벽돌을 포함하는 하나의 방법이었습니다.

3D 탐색 모드에서는 기초 바닥판 텍스쳐들(Base plate textures)의 로딩, 구조물들의 로딩, 구조물들의 애니메이션과 렌더링 등 꽤 많은 것들이 동시에 진행되었습니다. 이는 GPU와 CPU 모두를 엄청나게 요구했으므로 크롬 개발자도구에서 이들 영역을 가급적이면 많이 최적화하기 위해 많은 량의 프레임 프로파일링을 했습니다. 모바일 디바이스에서는 구조물에 꽤 가깝게 줌(Zoom)하도록 결정하였으므로 동시에 많은 구조물을 렌더링할 필요는 없었습니다.

몇몇 디바이스는 우리가 다시 살펴보고 WebGL 쉐이더의 일부를 단순화했지만 우리는 항상 문제의 해결과 전진을 위한 길을 발견할 수 있었습니다.

WebGL 기능이 없는 디바이스의 지원

방문자의 디바이스가 WebGL을 지원하지 않는다고 해도 약간은 사용이 가능한 사이트를 원했습니다. 때로는 캔버스를 통한 해결책이나 CSS3D 기능을 사용한 단순화된 방법으로 3D를 표현하는 방법들을 사용할 수 있습니다. 불행하게도 WebGL의 사용없이 빌드와 3D 탐색 기능을 유사하게 동작할 수 있는 충분히 훌륭한 해결방법을 찾지는 못했습니다.

일관성을 위해 구조물의 가시적인 스타일은 모든 플랫폼에서 동일해야 했습니다. 어쩌면 2.5D 방식을 통해 이를 시도해보았습니다만 이는 구조물들을 몇가지 방식에서 다르게 보이도록 만들었습니다. 또한 Build with Chrome의 첫번째 버전에서 만들어진 구조물을 어떻게 첫번째 버전과 동일하게 보이고 사이트의 새로운 버전에서 부드럽게 동작하는 것을 확실하게 지원할 것인가를 고려해야했습니다.

3D로 새로운 구조물을 만들거나 탐색할 수는 없지만 2D 탐색 모드는 여전히 WebGL을 지원하지 않는 디바이스에서 사용할 수 있습니다. 그러므로 사용자들은 여전히 프로젝트에서 깊이 있는 아이디어와 만약 WebGL이 가능한 디바이스라면 이 도구를 사용하여 생성한 무언가를 얻을 수도 있습니다. 사이트는 WebGL의 지원없이는 사용자에게 가치가 없을 것입니다만 최소한 티저 광고처럼 동작하거나 시도하도록 할 수는 있을 것입니다.

WebGL 솔루션에 대한 대체 버전을 유지하는 것은 때로는 가능하지 않을 수도 있습니다. 성능, 비주얼 스타일, 개발 그리고 유지보수 비용 등 많은 가능한 이유가 있습니다. 그러나, 대체물을 구현하지 않도록 결정하지 않았다면 최소한 WebGL을 사용할 수 있는 방문자를 처리해야 할 것이며, 왜 그들이 사이트를 완전하게 접근할 수 없고, 어떻게 이들을 WebGL을 지원하는 브라우저를 사용함으로써 문제를 어떻게 풀어야 할지 설명서를 주어야 할 것입니다.

애셋 관리

2013년 구글은 이 시작부터 가장 의미심장한 UI 변화를 가진 구글 맵스의 새로운 버전을 내놓았습니다. 새로운 구글 맵스의 UI와 맞도록 Build with Chome를 다시 새롭게 디자인하기로 하였으며 이리하여 다른 요인들을 새 디자인 속으로 가져왔습니다. 새로운 디자인은 깨끗한 단색과 단순한 형태를 사용하여 비교적 플랫(Flat)합니다. 이는 많은 UI 요소들 상에서 이미지의 사용을 최소화하고 순수한 CSS를 사용하는 것을 가능하게 만들었습니다.

탐색 시에 구조물들을 위한 썸네일 이미지, 기초 바닥판 그리고 최종적으로 실제 3D 구조물들을 위한 맵 텍스쳐들과 같은 많은 이미지들을 로딩할 필요가 있습니다. 우리는 끊임없이 새로운 이미지를 로딩할 때 메모리 릭이 없다는 것이 확실하도록 각별히 주의를 기울여야 합니다.

3D 구조물들은 커스텀 파일 포맷 내에 PNG 이미지처럼 패키지되어 저장됩니다. 3D 구조물들의 데이터를 이미지 형태로 저장하는 것은 기본적으로 바로 구조물들을 렌더링하는 쉐이더로 데이터를 바로 전달할 수 있도록 합니다.

모든 사용자가 이미지를 생성하기 위해 디자인은 모든 플랫폼들에 동일한 이미지 사이즈들을 사용할 수 있도록 하였으므로 저장소와 대역폭의 사용을 최소화하였습니다.

화면 방향 관리하기

세로 모드(Portrait mode)에서 가로 모드(Landscape mode)로 혹은 반대로 변경할 때 화면 비율이 얼마나 많이 변화하는지를 잊기 쉽습니다. 여러분은 모바일 디바이스에 적용을 시작할 때 이를 고려할 필요가 있습니다.

스크롤이 가능한 전통적인 웹사이트에서 컨텐츠와 메뉴들을 재정렬하는 반응형 사이트를 위해 CSS 규칙을 적용할 수 있을 것입니다. 여러분이 스크롤 기능을 사용하는 동안 이는 꽤 관리할만 합니다.

우리는 이 방법을 빌드와 함께 사용했지만 항상 보여지는 컨텐츠를 가질 필요가 있었고 많은 컨트롤과 버튼들에 대한 빠른 접근 방법을 여전히 가지고 있었기 때문에 어떻게 레이아웃을 해결할지에 대해서는 약간의 제약이 있었습니다. 뉴스 사이트 같은 순수한 컨텐츠 사이트를 위한 플루이드 레이아웃(Fluid layout)은 훌륭한 감각을 필요로 하지만 우리 것과 같은 게임 앱을 위해 이를 사용하는 것은 힘듭니다. 이는 여전히 컨텐츠의 흐름을 훌륭하게 유지하고 편안한 방식의 상호작용을 제공하면서 가로와 세로 방향으로 모두 동작하는 레이아웃을 찾기 위한 도전이 됩니다. 결국 빌드를 가로 모드만 유지하고 사용자에게 디바이스를 돌리라고 말해주기로 결정했습니다.

탐색은 양방향에서 훨씬 쉽게 해결되었습니다. 일관적인 경험을 제공하기 위한 방향 기반의 3D 줌(Zoom) 레벨의 조정이 필요했을 뿐입니다.

대부분의 컨텐츠 레이아웃은 CSS에 의해 제어됩니다만 몇가지 방향과 관련된 것들은 자바스크립트에서의 구현을 필요로 하였습니다. 방향을 판별하기 위해 window.orientation을 사용하는 것은 좋은 크로스-디바이스 솔루션이 아니라는 것을 확인하였기 때문에 결국 디바이스의 방향을 판별하기 위해 window.innerWidth와 window.innerHeight을 비교만 하였습니다.

if( window.innerWidth > window.innerHeight ){
  // 가로모드(landscape)
} else {
  // 세로모드(portrait)
}

터치 지원 추가하기

웹 컨텐츠에 대한 터치 기능의 추가 지원은 합리적이며 올바른 것입니다. 클릭 이벤트와 같은 기본 상호작용은 데스크탑이나 터치를 지원하는 디바이스에서 똑같이 동작하지만 보다 발전된 상호작용이 될 때는 touchstart, touchmove 그리고 touchend와 같은 터치 이벤트들 또한 다루는 것이 필요할 것입니다. 이 글은 이 이벤트들을 어떻게 사용하는지에 대한 기초들을 다루고 있습니다. 인터넷 익스플로러는 터치 이벤트를 지원하지 않는 대신 포인터 이벤트(Pointer Events) (pointerdown, pointermove, pointerup)를 사용합니다. 포인터 이벤트는 W3C에 표준으로 제출되었습니다만 현재는 인터넷 익스플로러에서만 구현되어 있습니다.

우리는 3D 탐색 모드에서 하나의 손가락을 사용하여 맵 주변을 보여주고 줌(Zoom)을 하기 위해 두 손가락의 핀치(Pinch, 꼬집는 형태의 제스쳐)를 사용하는 표준적인 구글 맵스 구현과 동일한 네비게이션을 원했습니다. 왜냐하면 구조물들 역시 우리가 두개의 손가락으로 회전 제스쳐를 추가하려고 하는 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);
    // 드래그를 구현하기 위해 필요한 모든 터치 이벤트에 대한 리스닝을 시작합니다.
    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();
    // 이벤트리스너의 갯수를 최소화하기 위해 touchstart를 제외하고 모든 이벤트리스너를 제거합니다.
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
    // 또한 탭들을 전환하거나 몇몇 다른 상황들에서의 예상치못한 동작을 피하기 위해 touchcancel 이벤트를 리스닝합니다.
    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;
  }
}

touchmove에 기반한 애니메이션을 할 때 마지막 이벤트로부터의 델타 이동 또한 저장하는 것이 종종 유용합니다. 예를 들어, 탐색 모드에서 기초 바닥판들 모두가 가로질러갈 때 카메라의 속도에 대한 인자로써 이를 사용할 수 있습니다. 기초 바닥판을 드래그할 수 없었다면 대신 카메라를 이동하고 있을 것입니다.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );

  // input.dragDX, input.dragDY, input.dragX 혹은 input.dragY에 기반하여 애니메이션을 실행합니다.
 /*
  /
  */

  // 실제 손가락이 이동할 때만 touchmove가 발생하므로 각 프레임마다 델타(Delta)값을 초기화할 필요가 있습니다.
  input.dragDX=0;
  input.dragDY=0;
}

첨부된 예제:

터치 이벤트를 이용하여 객체를 드래그하세요. 다음과 같이 Build with Chrome에서 3D 맵 탐색을 위해 드래그를 구현할 때와 비슷한 구현입니다.

멀티터치 제스쳐

이에 대해서는 멀티터치 제스쳐의 관리를 단순화하여 처리하는 HammerQuoJS와 같은 프레임워크들 혹은 라이브러리들이 있지만 완전한 컨트롤을 가지고 여러개의 제스쳐를 결합하기를 원한다면 때로는 가려운 곳을 긁는 것으로 시작하는 것이 최선입니다.

핀치(Pinch)와 회전 제스쳐를 관리하기 위해 두번째 손가락이 화면에 얹어질 때마다 두손가락 사이의 거리와 각도를 저장했습니다.

// 우리가 영향을 미치는 실제 객체의 스케일과/회전을 표현하는 변수
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;
  // 손가락 사이의 거리와 각도를 계산합니다.
  var dx = x2 - x1;
  var dy = y2 - y1;
  input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
  input.touchStartAngle=Math.atan2(dy,dx);
  // 또한 우리가 영향을 미치는 실제 객체의 현재 스케일과 회전값을 저장합니다.
  // 이는 점진적인 회전/스케일링을 지원하기 위해 필요합니다.
  // 제스쳐가 시작될 때 객체가 항상 동일한 스케일이라고 추정할 수는 없을 것입니다.
  input.startScale=currentScale;
  input.startAngle=currentRotation;
}

touchmove 이벤트가 발생하면 두 손가락의 거리와 각도를 지속적으로 측정합니다. 시작 거리와 현재 거리의 차이는 스케일을 설정하는데 사용되며 시작 각도와 현재 각도의 차이는 각도를 설정하는데 사용됩니다.

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){
    // 손가락 사이의 거리와 각도를 계산합니다.
    var dx = x2 - x1;
    var dy = y2 - y1;
    var touchDistance = Math.sqrt(dx*dx+dy*dy);
    var touchAngle = Math.atan2(dy,dx);
    // 현재 터치값과 시작값 사이의 거리를 계산합니다.
    var scalePixelChange = touchDistance - input.touchStartDistance;
    var angleChange = touchAngle - input.touchStartAngle;
    // 실제 객체에 얼마나 영향을 미칠지를 계산합니다.
    currentScale = input.startScale + scalePixelChange*0.01;
    currentRotation = input.startAngle+(angleChange*180/Math.PI);
    // 스케일의 상한선과 하한선
    if(currentScale<0.5) currentScale = 0.5;
    if(currentScale>3) currentScale = 3;
  }
}

드래그의 예와 비슷한 방식으로 어쩌면 각 touchmove 이벤트 사이의 거리 변화를 사용할 수 있습니다만 이 접근 방식은 지속적인 이동을 원할 때 종종 더 유용합니다.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  // currentScale과 currentRotation에 기반하여 변환(Transform)을 수행합니다.
  /*
  /
  */

  // 손가락이 실제로 이동 시에만 touchmove가 발생하므로 매 프레임마다 델타(Delta)값을 초기화합니다.
  input.dragDX=0;
  input.dragDY=0;
}

또한 원한다면 핀치(Pinch)와 회전 제스쳐를 하는 동안 객체의 드래그를 가능하게 할 수 있습니다. 이 경우 두 손가락의 중간 지점을 드래그 핸들러의 입력으로써 사용해야 할 것입니다.

첨부된 예제:

2D에서 객체 회전하기와 스케일링하기. 다음과 같이 탐색에서 어떻게 맵을 구현하였는지와 유사합니다.

동일한 하드웨어에서의 마우스와 터치 지원

오늘 날에는 마우스와 터치 입력을 둘 다 지원하는 크롬북 픽셀(Chromebook Pixel)과 같은 다양한 랩탑 컴퓨터가 존재합니다. 이는 주의를 기울이지 않으면 몇가지 예상치 못한 동작을 발생할 수 있습니다.

한가지 중요한 것은 터치 지원의 검출만으로 마우스 입력을 무시하는 대신 동시에 전부를 지원하여야 한다는 것입니다.

만약 터치 이벤트 핸들러에서 event.preventDefault()를 사용하지 않고 있다면 이는 또한 비-터치에 최적화된 대부분의 사이트가 여전히 동작하도록 유지하기 위해 몇가지 에뮬레이션된 마우스 이벤트들을 발생할 것입니다. 예를 들어, 화면 상에서의 단일 탭(Tab) 입력은 다음과 같은 이벤트들을 빠른 순서대로 발생할 것입니다.

  1. touchstart
  2. touchmove
  3. touchend
  4. mouseover
  5. mousemove
  6. mousedown
  7. mouseup
  8. click

만약 보다 복잡한 상호작용들을 가지고 있다면 이러한 마우스 이벤트들은 몇가지 예상치못한 동작들을 발생하고 여러분의 구현을 망가뜨릴 것입니다. 터치 이벤트 핸들러에서 event.preventDefault()의 사용이 종종 최선책이며 분리된 이벤트 핸들러에서 마우스 입력을 관리하게 할 것입니다. 또한 스크롤이나 클릭 이벤트와 같은 몇몇 기본 동작을 방지하기 위해 event.preventDefault()을 사용하는 것에 주의를 기울일 필요가 있습니다.

Build with Chrome에서는 사이트에서 누군가가 더블탭(Double-tap)을 했을 때 줌(Zoom)이 일어나는 것을 원하지 않았습니다. 이것이 모던 브라우저에서 표준이라고 할지라도 말이죠. 따라서 사용자의 더블 탭에 대해 줌이 일어나지 않도록 브라우저에 뷰포트(Viewport) 메타 태그를 사용했습니다. 이는 또한 사이트의 반응성을 향상시킬 수 있도록 300ms의 클릭 딜레이를 제거해줍니다. (클릭 딜레이는 더블탭 줌(Double-tap zooming)이 설정되어 있을 때 싱글탭(Single-tap)과 더블탭을 구분하기 위해 존재합니다.)

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

이 기능을 사용할 때는 사용자가 더 가까이 줌을 할 수 없기 때문에 사이트가 모든 화면 크기에서 가독성이 있도록 만드는 것은 여러분에게 달렸음을 기억하시기 바랍니다.

마우스, 터치 그리고 키보드 입력

3D 탐색 모드에서 맵을 네비게이션하는데는 마우스(드래그), 터치(드래그, 줌과 회전을 위한 핀치) 그리고 키보드(방향키를 이용한 네비게이션)의 3가지 방법이 있습니다. 이 모든 네비게이션 방법은 약간씩 다르게 동작하지만 우리는 다음과 같이 모든 방법에 대해 동일한 접근을 사용하였습니다. 이벤트 핸들러에 변수들을 설정하고 이를 requestAnimationFrame 루프에서 동작시킵니다. requestAnimationFrame 루프는 어떤 방법이 네비게이션을 위해 사용되는지 몰라도 됩니다.

예를 들자면 이 3가지 입력 방법 모두가 맵의 이동값(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;
}

// 이는 애니메이션이 실행되기 전의 매 프레임마다 호출되어야 합니다.
function handleKeyInput(){
  if(input.keyCodes.k37){
    input.dragDX = -5; // 37: 왼쪽 화살표키
  } else if(input.keyCodes.k39){
    input.dragDX = 5; //39: 오른쪽 화살표키
  } 
  if(input.keyCodes.k38){
    input.dragDY = -5; //38: 위쪽 화살표키
  } else if(input.keyCodes.k40){
    input.dragDY = 5; //40: 아래쪽 화살표키
  }
}

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  // keydown 이벤트가 매 프레임마다 발생하지 않으므로 키보드 상태를 먼저 처리할 필요가 있습니다.
  handleKeyInput();
  // 입력으로 무엇이 저장되었는지에 따라 애니메이션이 구현됩니다.
   /*
  /
  */

  //  실제 손가락이 이동할 때만 touchmove가 발생하므로 각 프레임마다 델타(Delta)값을 초기화할 필요가 있습니다.
  input.dragDX = 0;
  input.dragDY = 0;
}

첨부된 예제:

네비게이션을 위해 마우스, 터치 그리고 키보드를 사용하기

요약

다양하고 많은 화면 사이즈를 가진 터치 지원 디바이스를 지원하도록 Build with Chrome을 적용하는 것은 학습과도 마찬가지였습니다. 우리 팀은 터치 디바이스 상에서 이와 같은 수준의 상호작용을 하는데 아주 많은 경험을 가지고 있지 않았으며 이 기회를 통해 많은 것을 배웠습니다.

가장 큰 도전과제로 나타난 것은 어떻게 사용자 경험과 디자인을 해결할 수 있을까였습니다. 기술적인 도전은 많은 화면 크기, 터치 이벤트 그리고 성능 이슈들을 관리하는 것이었습니다.

터치 디바이스 상에서의 WebGL 쉐이더 사용에 대한 몇가지 도전과제들이 있었지만 이는 기대했던 것보다 꽤나 잘 동작했습니다. 디바이스들은 점점 더 강력해지고 있으며 WebGL 구현들은 빠르게 개선되고 있습니다. 우리는 우리가 가까운 시일 내에 디바이스에서 WebGL을 더 많이 사용할 것이라고 느끼고 있습니다.

아직 해보지 않으셨다면 이제 가셔서 놀라운 무언가를 만들어보시기 바랍니다!

Comments

0