Launch the Experiment

Google approached Big Spaceship in late 2013 to pitch a new experiment and brand identity for their I/O conference in 2014. As a team of three, consisting of UX, design and tech, we developed a generative branding concept that would inspire collaboration between web visitors and attendees.

As the team fleshed out visual concepts and user flows, I built an interactive prototype. We collaborated closely and iterated over multiple designs and working prototypes, both building off of each other’s ideas. After one week of hands-on iteration, the final output was a working product that connected users to collaborate in a visual experiment based on math and Web GL.

Responsibilities

  • Tech Lead
  • Prototyping
  • Visual Explorations
  • On-Site Presentation

Technologies

  • JavaScript
  • Web GL
  • GLSL
  • NodeJS

We presented the pitch in 2013 and got great feedback from the Google team on site. Ultimately, the I/O project itself went to another agency, but the proposal directly led to other projects in collaboration with Google and Big Spaceship.


Superformula in 2D

The core requirement for the visual prototype was that little user input would yield large, rewarding visual output and variety.

The Superformula provides exactly that: A small set of parameters that, when tweaked, can result in a vast amount of different shapes. The formula works on a plane in polar coordinates, modifying the radius based on angle.

This is what a naive implementation of the super-formula in JavaScript looks like:

/**
 * See http://en.wikipedia.org/wiki/Superformula
 * @param {number} angle Angle between 0 and 2*PI
 * @return {number} The resulting radius
 */
SuperFormula.prototype.getRadiusForAngle = function(angle) {
  var cos = Math.abs(Math.cos(angle * this.m * 0.25) / this.a);
  var sin = Math.abs(Math.sin(angle * this.m * 0.25) / this.b);
  var sum = Math.pow(cos, this.n2) + Math.pow(sin, this.n3);
  return Math.abs(Math.pow(sum, - 1 / this.n1));
};

Superformula in 3D

Two Super Formulas can be combined to create a three dimensional shape using spherical coordinates. While initial proof-of-concepts written entirely in JavaScript laid the foundation, I soon switched over to writing custom vertex and fragment shaders. That allowed us to smoothly animate thousands of vertices at 60fps. The following vertex shader modifies all vertices of a basic sphere geometry (I went for a tessellated Icosahedron) using two separate Super Formulas:

#define PI 3.14159265
#define TWOPI 6.28318531

#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vGlobalNormal;
varying vec3 vPosition;
varying vec4 vGlobalPosition;

uniform float shape1A;
uniform float shape1B;
uniform float shape1M;
uniform float shape1N1;
uniform float shape1N2;
uniform float shape1N3;

uniform float shape2A;
uniform float shape2B;
uniform float shape2M;
uniform float shape2N1;
uniform float shape2N2;
uniform float shape2N3;

float radiusForAngle(float angle, float a, float b, float m, float n1, float n2, float n3) {
  float tempA = abs(cos(angle * m * 0.25) / a);
  float tempB = abs(sin(angle * m * 0.25) / b);
  float tempAB = pow(tempA, n2) + pow(tempB, n3);
  return abs(pow(tempAB, - 1.0 / n1));
}

vec3 superPositionForPosition(vec3 p) {
  float r = length(p);

  float phi = atan(p.y, p.x);
  float theta = r == 0.0 ? 0.0 : asin(p.z / r);

  float superRadiusPhi = radiusForAngle(phi, shape1A, shape1B, shape1M, shape1N1, shape1N2, shape1N3);
  float superRadiusTheta = radiusForAngle(theta, shape2A, shape2B, shape2M, shape2N1, shape2N2, shape2N3);

  p.x = r * superRadiusPhi * cos(phi) * superRadiusTheta * cos(theta);
  p.y = r * superRadiusPhi * sin(phi) * superRadiusTheta * cos(theta);
  p.z = r * superRadiusTheta * sin(theta);

  return p;
}

void main() {
  vUv = uv;

  vNormal = normal;
  vGlobalNormal = normalize(normalMatrix * normal);

  vPosition = superPositionForPosition(position);
  vGlobalPosition = projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0);

  gl_Position = vGlobalPosition;
}

I leveraged THREE.js to do most of the grunt work, like providing basic geometries and compiling shaders, so I could focus on rendering the shape and adding collaborative functionality within the tight deadline. Additionally, using the library allowed me to use most of the same code to provide a lower polygon canvas-only version of the experiment for devices without WebGL support.

Bringing the Shape to Life

Simply rendering the super shape with default shading made it feel sterile and artificial. I spent a large amount of time on infusing personality and warmth into the final product by tweaking shading and adding animations. Instead of normals-based shading, I wrote a quick depth-based fragment shader that would blend two colors based on the distance to the center of the shape. This gave the output a much more organic look.

Flat normal based shading vs depth based shading

To add even more life to the shape, I attached a subtle pulsing animation that would tweak some of the shape’s parameters constantly over time. Layering a few sine curves on top of each other deformed the shape ever so slightly in regular intervals.

/**
 * Returns the amplitude of a pulse with two beats paired together.
 * @param {number} phase The phase for both beats (one beat per 2*PI)
 * @param {number} quickness Higher quickness yields shorter individual beats
 * @param {number} strengthA The strength of the first beat
 * @param {number} strengthB The strength of the second beat
 * @return {number} Amplitude ranging from 0 to 1
 */
Main.prototype.getPulse = function(phase, quickness, strengthA, strengthB) {
  var pulseA = 0.5 + 0.5 * Math.sin(phase); // 0...1
  var pulseB = 0.5 + 0.5 * Math.sin(phase - 0.333 * Math.PI); // 0...1
  pulseA = Math.pow(pulseA, quickness);
  pulseB = Math.pow(pulseB, quickness);
  return strengthA * pulseA + strengthB * pulseB;
};

Below is a sample pulse plot with the following values:

f(x) = 15 * ((0.5 + 0.5 * sin(x)) ^ 24) + 10 * ((0.5 + 0.5 * sin(x - 0.333 * pi)) ^ 24)

Sample Pulse Plot

Collaboration

One of the core principles of the experiment was that users would be able to land on the site and initially have a limited set of tools. They could then invite other people to join their session. Each user would have a different, unique set of tools to modify the shape, exponentially increasing the variety of visual output.

I used socket.io and the Google+ SDK to connect users through a Google Compute Engine box running NodeJS. The server would store all super shape parameters per session. Users could join a session and the server would assign them to a set of shape parameters that they could control. Once a parameter changed, it would be broadcasted to all other users in the session.

Supershape Collaboration Screen Recording

All users participating in the current session were shown in a sidebar with their profile pictures. To make it clear to other users who was modifying which aspect of the shape, each user profile would animate as soon as they sent changes through the server.

API

Another idea that came up was to allow for developer easter eggs and extensions. As part of that, the prototype features a RESTful API, which we demoed during the interactive presentation. Through the API, users would be able to trigger shape updates via simple HTTP requests.

The following requests actually work and change the super shape at supershapes.bigspaceship.com. You can copy and paste the requests into a terminal to see it running.

Sample GET Request

curl http://supershapes.bigspaceship.com/api

Sample GET Response

{
  "settings": {
    "shape1.a": 1,
    "shape1.b": 1,
    "shape1.m": 18,
    "shape1.n1": 2,
    "shape1.n2": 1.8,
    "shape1.n3": 2.5,
    "shape2.a": 1,
    "shape2.b": 1,
    "shape2.m": 0,
    "shape2.n1": 2,
    "shape2.n2": 4,
    "shape2.n3": 2,
    "pulse.speed": 8,
    "pulse.flush": 0.33,
    "pulse.contraction": 1,
    "pulse.beatA": 0.1,
    "pulse.beatB": 0.15,
    "color.background.h": 0.5046296296296297,
    "color.bright.h": 0.12264150943396228,
    "color.dark.h": 0.01293103448275862,
    "shading.minColorDistance": 0,
    "shading.maxColorDistance": 1.25,
    "shading.lightIntensity": 1,
    "shading.ambientLightIntensity": 0,
    "shading.brightnessMultiplier": 1,
    "shading.smooth": true,
    "animation.snappiness": 0.05,
    "particles.enabled": false
  }
}

Sample POST request

curl -X POST -H "Content-Type: application/json" -d '{"shape1.m": 8, "shape2.m": 6}' http://supershapes.bigspaceship.com/api/settings

Sample POST response

{
  "success": true,
  "settings": {
    "shape1.a": 1,
    "shape1.b": 1,
    "shape1.m": 8,
    "shape1.n1": 2,
    "shape1.n2": 1.8,
    "shape1.n3": 2.5,
    "shape2.a": 1,
    "shape2.b": 1,
    "shape2.m": 6,
    "shape2.n1": 2,
    "shape2.n2": 4,
    "shape2.n3": 2,
    "pulse.speed": 8,
    "pulse.flush": 0.33,
    "pulse.contraction": 1,
    "pulse.beatA": 0.1,
    "pulse.beatB": 0.15,
    "color.background.h": 0.5046296296296297,
    "color.bright.h": 0.12264150943396228,
    "color.dark.h": 0.01293103448275862,
    "shading.minColorDistance": 0,
    "shading.maxColorDistance": 1.25,
    "shading.lightIntensity": 1,
    "shading.ambientLightIntensity": 0,
    "shading.brightnessMultiplier": 1,
    "shading.smooth": true,
    "animation.snappiness": 0.05,
    "particles.enabled": false
    // ...
  }
}

Team

  • Naim Sheriff (Design)
  • Livia Veneziano (UX)
  • Grace Steite (Production)