Latest

Behind the Code —The New eBay 

eBay recently released a brand new version of their mobile app, eBay 4.0. More than an update, the new mobile app was a consolidation of several apps they’d been developing for multiple platforms. Instrument was given the opportunity to design and develop a website announcing the new app and giving an overview of its new features.

The site was designed to be a single page scroll, but we wanted to allow deep-linking and analyze visitor usage. Dynamically updating the URL as the user makes their way through the experience allowed us to do both. Additionally, we had the challenge of localizing the site with custom content for 24 locales.

Build Architecture

The site was developed in Ruby, using Middleman as our static site generator, and pulling content from Contentful. Contentful allowed us to create custom content types as well as specify fields that needed to be localized. Rake tasks were used to pull down all of the Contentful data and assets that were then organized into YAML files for each locale. With one localizable template and some logic, 24 independent static pages were created with the correct data based on the locales we had configured.

  1. This is the process of pulling down content from Contentful using a rake task. The content types are stored in YAML files under a locale directory.
     
  2. Here, the index.html file is used as a template. The hero[:headline] tag pulls down data from the YAML file to populate the template with localized strings. 
     
  3. The Middleman build produces static files based on your supported languages.

This has become a very successful approach for us. This stack allowed us to be in complete control of the backend, while giving our clients an easy-to-navigate system to manage copy, assets and translations. With a little help from Webpack, we were also able to leverage the powerful new features of JavaScript ES2015 like classes, arrows and decorators.

Header Animation

One challenge of the site was the animation at the top of the page. Our design team had communicated the concept with terms like “cloud” and “blizzard.” We used the glmatrix library, combined with 3D CSS, to place and animate the objects in 3D space and then subtly tie them to the location of the mouse to provide a fun interaction after the initial explosion animation. 

To setup the product cloud, we built arrays representing the larger products, then added a second array of the colorful children’s blocks to fill in the spaces around the larger items. A ratio relative to the screen size allows us to display more items on larger screens without crowding the cloud on mobile.

const visibleProducts = allProducts.slice(0, allProducts.length - Math.round(11 * ratio));
const visibleBlocks = allBlocks.slice(0, blocks.length - Math.round(28 * ratio));

A spokeLength variable was built to create enough space in the middle of the product cloud for our welcome mat content. This length changed based on the screen width as well as the number of products being sent to the page.

  this.products = visibleProducts.map((name, i) => {
     const rotation = increments * i;

     let spokeLength;

     if (i % 3 === 0) {
       if ($(window).width() <= 1400 || $(window).height() <= 800) {
         spokeLength = 0.7;
       } else {
         spokeLength = 0.6;
       }
     } else if (i % 3 === 1) {
       spokeLength = 0.8;
     } else if (i % 3 === 2) {
       spokeLength = 1.0;
     }

     const vec = [0, spokeLength, 0];

     const mat = mat4.create();
     mat4.rotateZ(mat, mat, rotation);

     vec3.transformMat4(vec, vec, mat);

     return {
       vec: vec,
       element: $(this.container).find(`.item--${name}`).get(0),
       scale: 0.7,
     };
   });

   /**
    * Generate positions and setup sizing for blocks.
    */
   this.products = this.products.concat(visibleBlocks.reduce((sum, name) => {
     const elems = $(this.container).find(`.item--${name}`).toArray();
     const defs = elems.map((elem) => {
       return {
         vec: this.randomSpherePosition(),
         element: elem,
         scale: 0.15,
       };
     });

     return sum.concat(defs);
   }, []));
 }
Scrolling Animation

As the user scrolls down the page, we used CSS transforms to place the objects on 1 of 3 layers of perspective, so the experience would have a tangible depth. The closest objects move faster than the mouse scroll, while the furthest objects move slower.

This site also called for moments on the page where content freezes in place and hidden elements are revealed. Our goal was to avoid scroll-jacking at all costs, so we used a combination of ScrollMagic and Greensock to fix elements in position while letting the user retain control of native scrolling. 

First, we set up some variables for the elements we’d be animating. The ‘feature’ constant represented the text and the ‘screen’ represented, you guessed it, the screen inside the device. The initial feature and screen was ‘primary’ and the second was ‘secondary’. 

export function activityAnimation(element) {
  const controller = new ScrollMagic.Controller();
  const $element = $( element );
  const $featurePrimary = $element.find('.js-feature-primary');
  const $featureSecondary = $element.find('.js-feature-secondary');
  const $screenPrimary = $element.find('.js-screen-primary');
  const $screenSecondary = $element.find('.js-screen-secondary');
  const $device = $element.find('.js-feature-device');
};

We calculated the element height so we could center the device in the browser and used the ‘navHeight’ for additional vertical offsetting.

This is where things got tricky and the trial & error refinement began. Our goal was to create a scrolling experience where the user could move at a natural speed and catch the transition animations while at the same time allowing them to zip past the sections with no lag or strange artifacts.

We used pixels for our duration rather than time and landed on 800px as the right amount of height for a smooth experience. Basically, the scene would stick in fixed position for 800px of scrolling and then it would be released.

 const pinDuration = 800;

 const scenePinSection = new ScrollMagic.Scene({
   triggerElement: element,
   duration: pinDuration,
   reverse: true,
   offset: elementYCenter,
 })
 .setPin(element);

The transitions between features and screens, on the other hand, we’re built to change with a 400ms fixed time. We went this route to avoid any strange screen overlap or dead space in the experience, also to mimic the transitions within the actual Ebay app we were promoting. Greensock’s TweenMax library gave us everything we needed to make these animations happen on a pixel-based timeline.

 const tweenDuration = 0.4;
 const tweenEasing = Quad.easeInOut;

 /** Tween used in the sceneFeatureSwitch scene */
 const tweenFeature = new TimelineMax()
   .fromTo(
     $featurePrimary,
     tweenDuration,
     {y: 0, opacity: 1, ease: tweenEasing},
     {y: -20, opacity: 0, ease: tweenEasing},
     0
   )
   .fromTo(
     $featureSecondary,
     tweenDuration + 0.1,
     {y: 20, opacity: 0, ease: tweenEasing},
     {y: 0, opacity: 1, ease: tweenEasing},
     tweenDuration
   )
   .fromTo(
     $screenPrimary,
     tweenDuration,
     {opacity: '1', ease: tweenEasing},
     {opacity: '0', ease: tweenEasing},
     0
   )
   .fromTo(
     $screenSecondary,
     tweenDuration + 0.1,
     {opacity: '0', ease: tweenEasing},
     {opacity: '1', ease: tweenEasing},
     tweenDuration
   );

Once the transitions were built, we had to find the right time to trigger them. We found that it was more natural to call the animation early in the 800px duration. The trigger was set at ⅓ of the way through, giving the user a little more air beneath the animation to take in the information and screenshots. Once the transition occurred, floating products from eBay were used to key the user that movement was still occurring despite the fixed device.

Another challenge was to give the visual elements the ability to resize with the browser in a fluid fashion. We used percentage rather than hard pixels to place the screens inside the devices. We wrote this pretty little mixin to help with multiple devices and ratios.

@mixin aspect-ratio($width, $height, $position) {
  position: $position;

  &:before {
    display: block;
    content: "";
    width: 100%;
    padding-top: ($height / $width) * 100%;
  }

  > .content {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}

And called the mixin like this:

&--iphone {
  @include aspect-ratio(612, 1221, relative);
  max-width: 305px;
  background-image: image-url("devices/iphone.png");
}
Conclusion

At the end of the day, we were able to do our job of bringing excitement to the new app, explaining high-level features, and building functionality into the site that can be used in the future.


Written by Taryn Delhotal & Scot Mortimer, Developers

Related