Latest

Behind the Code — TEDxPortland’s Wonderland

TEDxPortland partnered with Instrument to define, design and develop their theme for 2016: Wonderland. In a word, Wonderland is Portland. It’s the surrounding natural beauty and the progressive community of people. The powerful sum of these parts makes this place endlessly unique and downright special. Our goal in creating Wonderland was to highlight the power of perspective—not only the importance of shifting our own perspectives, but to acknowledge and empathize with all of the perspectives around us.
Approach

For the website, a distorted and fluid canvas was chosen as a means to view the beauty of Portland through the glorious chaos of so many perspectives. A black and white cloud-like image, or displacement map, is being repeated and animated behind the image, pushing the pixels around like positive and negative magnets.

To accomplish this, we leaned on a 2D webGL rendering engine called PIXI.js for the animation and filters. Below, we’ll show you some of the code used to create a JavaScript class we call Jello.js. We’ll also be leaning on the Greensock Animation Platform (TweenLite) to help with some of the transitions and easing.

Development

The displacement map is a blurred out, repeating, black and white cloud-like image created in Photoshop. It was finalized at 2048x2048px and optimized down to around 100kb. It’s important that the displacement map is as small as possible for fast loading and distortion rendering. This image is repeating and animating up, down, left, right and rotating. Not to change the subject but I think I just got 50 lives in Contra.

PIXI is a container-based canvas rendering engine. We used ES2015 JavaScript to build a container for the images (this.container), a container for the images and distortion to interact (this.stage), and a renderer to make it all work together. The final renderer is then appended to the DOM in a canvas element and requestAnimationFrame is used to put it all into motion.

Once the animation is going, you can make changes to the this.options.transition variable to clarify or distort the image (from 0 to 1). The this.options.speed variable will increase the speed of the distortion animation or reverse the animation if you go into negative numbers.

Distortion constructor

constructor ( options = {} ) {
  this.defaults = {};
  this.options = options;
  this.canvasHolder = document.getElementById( "jello-container" );
  this.imgWidth = 1920;
  this.imgHeight = 960;
  this.imgRatio = this.imgHeight / this.imgWidth;
  this.winWidth = window.innerWidth;
  this.bgArray = [];
  this.bgSpriteArray = [];
  this.renderer = window.PIXI.autoDetectRenderer( this.winWidth, (this.winWidth * this.imgRatio) );
  this.stage = new window.PIXI.Container();
  this.imgContainer = new window.PIXI.Container();
  this.imageCounter = 0;
  this.displacementSprite = window.PIXI.Sprite.fromImage( "/assets/images/codes/jello/dmap-clouds-01.jpg" );
  this.displacementFilter = new window.PIXI.filters.DisplacementFilter( this.displacementSprite );
  this.currentMap = {};
  this.mapCounter = 0;
  this.mapArray = [];
  this.raf = this.animateFilters.bind( this );
  this.isDistorted = true;
  this.isTransitioning = false;
  this.initialize();
}

Initialize the distortion

initialize () {
  this.defaults = {
      transition: 1,
      speed: 0.5,
      dispScale: 200,
      dispX: true,
      dispY: true,
      count: 0
  };
  this.update();
  // An array of images for background (.jpg)
  // They"ll transition in the order listed below
  this.bgArray.push(
      "image-1",
      "image-2"
  );
  // An array of displacement maps
  // They"ll transition in the order below with the included settings
  this.mapArray.push(
      {
          image: "dmap-clouds-01.jpg",
          speed: 0.5,
          scale: 200
      },
      {
          image: "dmap-glass-01.jpg",
          speed: 0.3,
          scale: 200
      }
  );
  this.backgroundFill();
  this.buildStage();
  this.createBackgrounds();
  this.createFilters();
  this.animateFilters();
  this.eventListener();
  this.renderer.view.setAttribute( "class", "jello-canvas" );
  this.canvasHolder.appendChild( this.renderer.view );
}

Create the backgrounds

createBackgrounds () {
  this.bgArray.map( ( image ) => {
      const bg = window.PIXI.Sprite.fromImage( `/assets/images/codes/jello/${image}.jpg` );

      // Set image anchor to the center of the image
      bg.anchor.x = 0.5;
      bg.anchor.y = 0.5;
      this.imgContainer.addChild( bg );
      this.bgSpriteArray.push( bg );
      // set first image alpha to 1, all else to 0
      bg.alpha = this.bgSpriteArray.length === 1 ? 1 : 0;
  });
}

Create the filters

createFilters () {
  this.stage.addChild( this.displacementSprite );
  this.displacementFilter.scale.x = this.displacementFilter.scale.y = this.winWidth / this.imgWidth;
  this.imgContainer.filters = [
      this.displacementFilter
  ];
}

Build the stage

buildStage () {
  this.imgContainer.position.x = this.imgWidth / 2;
  this.imgContainer.position.y = this.imgHeight / 2;
  this.stage.scale.x = this.stage.scale.y = this.winWidth / this.imgWidth;
  this.stage.interactive = true;
  this.stage.addChild( this.imgContainer );
}

Animate the filters

animateFilters () {
  this.displacementFilter.scale.x = this.settings.dispX ? this.settings.transition * this.settings.dispScale : 0;
  this.displacementFilter.scale.y = this.settings.dispY ? this.settings.transition * (this.settings.dispScale + 10) : 0;
  this.displacementSprite.x = Math.sin( this.settings.count * 0.15 ) * 200;
  this.displacementSprite.y = Math.cos( this.settings.count * 0.13 ) * 200;
  this.displacementSprite.rotation = this.settings.count * 0.06;
  this.settings.count += 0.05 * this.settings.speed;
  this.renderer.render( this.stage );
  window.requestAnimationFrame( this.raf );
}

Written by Scot Mortimer, Developer

Related