우수사례 - Google I/O 2013 실험

토마스 레이놀즈
토마스 레이놀즈

소개

컨퍼런스 등록이 시작되기 전에 Google I/O 2013 웹사이트에 개발자의 관심을 유도하기 위해 Google은 터치 상호작용, 생성형 오디오, 발견의 즐거움에 초점을 맞춘 일련의 모바일 중심 실험 및 게임을 개발했습니다. 코드의 잠재력과 플레이의 힘에서 영감을 받은 이 대화형 경험은 새로운 I/O 로고를 탭할 때 "I"와 "O"의 단순한 소리로 시작됩니다.

자연 움직임

우리는 HTML5 상호작용에서는 흔히 볼 수 없는 흔들리고 유기적인 효과로 I 및 O 애니메이션을 구현하기로 결정했습니다. 재미와 반응성을 높이기 위해 옵션을 바꾸는 데 약간의 시간이 걸렸습니다.

탄성 물리학 코드 예

이 효과를 얻기 위해 우리는 두 도형의 모서리를 나타내는 일련의 점에 대한 간단한 물리 시뮬레이션을 실행했습니다. 두 도형 중 하나를 탭하면 탭한 위치에서 모든 점이 가속됩니다. 이 고양이는 늘어났다가 다시 끌어당깁니다.

인스턴스화할 때 각 지점은 임의의 가속량을 얻고 리바운드 '탄성'을 가지므로 다음 코드에서 볼 수 있듯이 균일하게 애니메이션되지 않습니다.

this.paperO_['vectors'] = [];

// Add an array of vector points and properties to the object.
for (var i = 0; i < this.paperO_['segments'].length; i++) {
  var point = this.paperO_['segments'][i]['point']['clone']();
  point = point['subtract'](this.oCenter);

  point['velocity'] = 0;
  point['acceleration'] = Math.random() * 5 + 10;
  point['bounce'] = Math.random() * 0.1 + 1.05;

  this.paperO_['vectors'].push(point);
}

그런 다음 탭하면 아래의 코드를 사용하여 탭한 위치로부터 바깥쪽으로 가속됩니다.

for (var i = 0; i < path['vectors'].length; i++) {
  var point = path['vectors'][i];
  var vector;
  var distance;

  if (path === this.paperO_) {
    vector = point['add'](this.oCenter);
    vector = vector['subtract'](clickPoint);
    distance = Math.max(0, this.oRad - vector['length']);
  } else {
    vector = point['add'](this.iCenter);
    vector = vector['subtract'](clickPoint);
    distance = Math.max(0, this.iWidth - vector['length']);
  }

  point['length'] += Math.max(distance, 20);
  point['velocity'] += speed;
}

마지막으로, 코드에서 다음 접근 방식을 사용하면 모든 입자가 모든 프레임에서 감속되고 천천히 평형 상태로 돌아갑니다.

for (var i = 0; i < path['segments'].length; i++) {
  var point = path['vectors'][i];
  var tempPoint = new paper['Point'](this.iX, this.iY);

  if (path === this.paperO_) {
    point['velocity'] = ((this.oRad - point['length']) /
      point['acceleration'] + point['velocity']) / point['bounce'];
  } else {
    point['velocity'] = ((tempPoint['getDistance'](this.iCenter) -
      point['length']) / point['acceleration'] + point['velocity']) /
      point['bounce'];
  }

  point['length'] = Math.max(0, point['length'] + point['velocity']);
}

자연 모션 데모

I/O 홈 모드를 사용해 보세요. 또한 이 구현에 여러 가지 추가 옵션이 노출되었습니다. '점 표시'를 켜면 물리 시뮬레이션과 힘이 작용하는 개별 지점이 표시됩니다.

리스키닝

재택 모드 모션에 만족한 나머지 두 가지 레트로 모드인 8비트와 Ascii에도 동일한 효과를 사용하고 싶었습니다.

이 리스킨닝을 수행하기 위해 홈 모드의 동일한 캔버스를 사용하고 픽셀 데이터를 사용하여 두 가지 효과를 각각 생성했습니다. 이 접근 방식은 장면의 각 픽셀을 검사하고 조작하는 OpenGL 프래그먼트 셰이더를 연상시킵니다. 이에 대해 좀 더 자세히 살펴보겠습니다.

캔버스 '셰이더' 코드 예

캔버스의 픽셀은 getImageData 메서드를 사용하여 읽을 수 있습니다. 반환된 배열에는 각 픽셀의 RGBA 값을 나타내는 픽셀당 4개의 값이 포함됩니다. 이러한 픽셀은 거대한 배열 형태의 구조로 함께 연결되어 있습니다. 예를 들어 2x2 캔버스는 imageData 배열에 4픽셀과 16개의 항목이 있습니다.

캔버스는 전체 화면이므로 iPad에서와 같이 화면이 1024x768이라고 가정하면 배열에는 3,145,728개 항목이 있습니다. 애니메이션이므로 전체 배열은 초당 60회 업데이트됩니다. 최신 JavaScript 엔진은 프레임 속도를 일관되게 유지하기에 충분할 정도로 많은 양의 데이터에 대한 루프 처리와 조치를 신속하게 처리할 수 있습니다. (도움말: 해당 데이터를 개발자 콘솔에 기록하지 마세요. 브라우저의 크롤링 속도가 느려지거나 완전히 다운될 수 있습니다.)

8비트 모드가 홈 모드 캔버스를 읽고 픽셀을 날려 차단 효과가 있도록 하는 방법은 다음과 같습니다.

var pixelData = pctx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);

// tctx is the Target Context for the output Canvas element
tctx.clearRect(0, 0, targetCanvas.width + 1, targetCanvas.height + 1);

var size = ~~(this.width_ * 0.0625);

if (this.height_ * 6 < this.width_) {
 size /= 8;
}

var increment = Math.min(Math.round(size * 80) / 4, 980);

for (i = 0; i < pixelData.data.length; i += increment) {
  if (pixelData.data[i + 3] !== 0) {
    var r = pixelData.data[i];
    var g = pixelData.data[i + 1];
    var b = pixelData.data[i + 2];
    var pixel = Math.ceil(i / 4);
    var x = pixel % this.width_;
    var y = Math.floor(pixel / this.width_);

    var color = 'rgba(' + r + ', ' + g + ', ' + b + ', 1)';

    tctx.fillStyle = color;

    /**
     * The ~~ operator is a micro-optimization to round a number down
     * without using Math.floor. Math.floor has to look up the prototype
     * tree on every invocation, but ~~ is a direct bitwise operation.
     */
    tctx.fillRect(x - ~~(size / 2), y - ~~(size / 2), size, size);
  }
}

8비트 셰이더 데모

아래에서는 8비트 오버레이를 제거하고 그 아래에 원래 애니메이션을 표시합니다. '화면 종료' 옵션은 소스 픽셀을 잘못 샘플링하여 발견한 이상한 효과를 보여줍니다. 우리는 8비트 모드의 크기가 적절하지 않은 가로세로 비율로 조정되었을 때 '반응형' 이스터 에그로 사용했습니다. 다행입니다!

캔버스 합성

여러 개의 렌더링 단계와 마스크를 결합하여 놀라운 결과를 얻을 수 있습니다. Google에서는 각 공에 고유한 방사형 그라데이션이 있고 공이 겹치는 부분에 이러한 그라데이션이 혼합되어야 하는 2D 메타볼을 만들었습니다. (아래 데모에서 볼 수 있습니다.)

이를 위해 두 개의 개별 캔버스를 사용했습니다. 첫 번째 캔버스에서는 메타볼 도형을 계산하고 그립니다. 두 번째 캔버스에서는 각 볼의 위치에 방사형 그래디언트를 그립니다. 그런 다음 도형이 그라데이션을 마스킹하고 최종 출력을 렌더링합니다.

합성 코드 예

다음은 모든 작업을 실행하는 코드입니다.

// Loop through every ball and draw it and its gradient.
for (var i = 0; i < this.ballCount_; i++) {
  var target = this.world_.particles[i];

  // Set the size of the ball radial gradients.
  this.gradSize_ = target.radius * 4;

  this.gctx_.translate(target.pos.x - this.gradSize_,
    target.pos.y - this.gradSize_);

  var radGrad = this.gctx_.createRadialGradient(this.gradSize_,
    this.gradSize_, 0, this.gradSize_, this.gradSize_, this.gradSize_);

  radGrad.addColorStop(0, target['color'] + '1)');
  radGrad.addColorStop(1, target['color'] + '0)');

  this.gctx_.fillStyle = radGrad;
  this.gctx_.fillRect(0, 0, this.gradSize_ * 4, this.gradSize_ * 4);
};

그런 다음 마스킹 및 그리기를 위해 캔버스를 설정합니다.

// Make the ball canvas the source of the mask.
this.pctx_.globalCompositeOperation = 'source-atop';

// Draw the ball canvas onto the gradient canvas to complete the mask.
this.pctx_.drawImage(this.gcanvas_, 0, 0);
this.ctx_.drawImage(this.paperCanvas_, 0, 0);

결론

우리가 사용할 수 있는 다양한 기술과 우리가 구현한 기술 (예: 캔버스, SVG, CSS 애니메이션, JS 애니메이션, 웹 오디오 등) 덕분에 프로젝트를 매우 재미있게 개발할 수 있었습니다.

여기 보이는 것 외에도 더 많은 것을 탐색할 수 있습니다. I/O 로고를 계속 탭해 보세요. 올바른 시퀀스를 사용하면 더 많은 미니 실험, 게임, 환상적인 비주얼은 물론 아침 식사도 맛볼 수 있습니다. 최적의 감상 환경을 위해 스마트폰이나 태블릿에서 시도해 보시기 바랍니다.

먼저 O-I-I-I-I-I-I-I 조합으로 시작해 보세요. 지금 사용해 보기: google.com/io

오픈소스

Google은 코드 Apache 2.0 라이선스를 오픈소스로 공개했습니다. GitHub(http://github.com/Instrument/google-io-2013)에서 확인하실 수 있습니다.

크레딧

개발자:

  • 토마스 레이놀즈
  • 브라이언 헤프터
  • 스테파니 부화
  • 폴 파닝

디자이너:

  • 댄 셰크터
  • 세이지 브라운
  • 카일 벡

제작자:

  • 아미 파스칼
  • 안드레아 넬슨