Пример использования: эксперимент Google I/O 2013

Томас Рейнольдс
Томас Рейнольдс

Введение

Чтобы привлечь внимание разработчиков к веб-сайту Google I/O 2013 до открытия регистрации на конференцию, мы разработали серию мобильных экспериментов и игр, ориентированных на сенсорное взаимодействие, генеративный звук и радость открытий. Этот интерактивный опыт, вдохновленный потенциалом кода и игровыми возможностями, начинается с простых звуков «I» и «O», когда вы касаетесь нового логотипа ввода-вывода.

Органическое движение

Мы решили реализовать анимацию ввода и вывода с шатким, органичным эффектом, который не часто можно увидеть во взаимодействиях HTML5. Выбор вариантов, чтобы игра была веселой и реактивной, заняла немного времени.

Пример кода упругой физики

Чтобы добиться этого эффекта, мы использовали простое физическое моделирование ряда точек, представляющих края двух фигур. При касании любой формы все точки ускоряются от места касания. Они вытягиваются и удаляются, прежде чем втягиваются обратно.

При создании экземпляра каждая точка получает случайную величину ускорения и «прыгучесть», поэтому они не анимируются равномерно, как вы можете видеть в этом коде:

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']);
}

Демонстрация органического движения

Вот домашний режим ввода-вывода, с которым вы можете поиграть. Мы также представили несколько дополнительных опций в этой реализации. Если вы включите «показывать точки», вы увидите отдельные точки, на которые действует физическое моделирование и силы.

Рескиннинг

Когда нас устроило движение в домашнем режиме, мы захотели использовать тот же эффект для двух ретро-режимов: Eightbit и Ascii.

Чтобы выполнить эту смену оформления, мы использовали тот же холст из домашнего режима и использовали данные пикселей для создания каждого из двух эффектов. Этот подход напоминает фрагментный шейдер OpenGL, где проверяется и обрабатывается каждый пиксель сцены. Давайте углубимся в это подробнее.

Пример кода Canvas «Shader»

Пиксели на Canvas можно прочитать с помощью метода getImageData . Возвращенный массив содержит 4 значения на пиксель, представляющие значение RGBA каждого пикселя. Эти пиксели объединены в массивную структуру, похожую на массив. Например, холст размером 2x2 будет иметь 4 пикселя и 16 записей в массиве imageData.

Наш холст полноэкранный, поэтому если мы представим, что экран имеет разрешение 1024x768 (как на iPad), тогда массив будет содержать 3 145 728 записей. Поскольку это анимация, весь этот массив обновляется 60 раз в секунду. Современные механизмы JavaScript могут обрабатывать циклы и обрабатывать такой большой объем данных достаточно быстро, чтобы поддерживать постоянную частоту кадров. (Совет: не пытайтесь записывать эти данные в консоль разработчика, так как это замедлит сканирование вашего браузера или приведет к его полному сбою.)

Вот как наш восьмибитный режим считывает холст домашнего режима и увеличивает пиксели, чтобы получить более блочный эффект:

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);
  }
}

Демонстрация восьмибитного шейдера

Ниже мы удалим наложение Eightbit и увидим оригинальную анимацию. Опция «экран уничтожения» покажет вам странный эффект, с которым мы столкнулись при неправильной выборке исходных пикселей. В конечном итоге мы использовали его как «отзывчивую» пасхалку, когда размер режима Eightbit был изменен до маловероятных соотношений сторон. Счастливого случая!

Композитинг холста

Удивительно, чего можно добиться, комбинируя несколько этапов рендеринга и масок. Мы создали двухмерный меташар, который требует, чтобы каждый шар имел свой собственный радиальный градиент и эти градиенты смешивались вместе там, где шары перекрываются. (Вы можете увидеть это в демо ниже.)

Для этого мы использовали два отдельных холста. Первый холст вычисляет и рисует форму метабола. Второй холст рисует радиальные градиенты в каждой позиции шара. Затем форма маскирует градиенты, и мы визуализируем окончательный результат.

Пример кода композиции

Вот код, который делает все возможным:

// 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);

Заключение

Разнообразие методов, которые нам пришлось использовать, и реализованных нами технологий (таких как Canvas, SVG, CSS Animation, JS Animation, Web Audio и т. д.) сделали разработку проекта невероятно интересной.

Здесь можно исследовать гораздо больше, чем то, что вы видите здесь. Продолжайте нажимать на логотип ввода-вывода, и правильные последовательности откроют больше мини-экспериментов, игр, необычных визуальных эффектов и, возможно, даже некоторых блюд на завтрак. Мы предлагаем вам попробовать его на своем смартфоне или планшете, чтобы получить наилучшие впечатления.

Вот комбинация для начала: OIIIIIII. Попробуйте прямо сейчас: google.com/io

Открытый источник

Мы открыли исходный код лицензии Apache 2.0. Вы можете найти его на нашем Github по адресу: http://github.com/Instrument/google-io-2013 .

Кредиты

Разработчики:

  • Томас Рейнольдс
  • Брайан Хефтер
  • Стефани Хэтчер
  • Пол Фарнинг

Дизайнеры:

  • Дэн Шехтер
  • Сейдж Браун
  • Кайл Бек

Продюсеры:

  • Эми Паскаль
  • Андреа Нельсон