Close Menu

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    How to create Social media popup hover menu using HTML and CSS

    25 June 2025

    How to create Valentine’s Love Button Animation using HTML CSS and JS

    23 June 2025

    How to create Magic Indicator Menu using HTML CSS and JS

    20 June 2025
    Facebook X (Twitter) Instagram YouTube Telegram Threads
    Coding StellaCoding Stella
    • Home
    • Blog
    • HTML & CSS
      • Login Form
    • JavaScript
    • Hire us!
    Coding StellaCoding Stella
    Home - JavaScript - How to create Animated Rubik Cube using HTML CSS and JS
    JavaScript

    How to create Animated Rubik Cube using HTML CSS and JS

    Coding StellaBy Coding Stella3 June 2025Updated:9 June 2025No Comments67 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Reddit Email WhatsApp Copy Link

    Let’s create an Animated Rubik’s Cube using HTML, CSS, and JavaScript! 🧊✨ This fun and interactive project brings the classic Rubik’s Cube to life with smooth 3D animations and colorful cube faces.

    We’ll use:

    • HTML to build the cube’s structure.
    • CSS for 3D styling, coloring, and animation.
    • JavaScript to add movement and interactivity (like spinning or auto-rotation).

    Whether you’re a beginner or a coding pro, this project is a great way to explore 3D transforms, animation, and creative UI elements. Let’s bring the Rubik’s Cube to your screen with code!

    HTML :

    This HTML code creates the layout for a Rubik’s Cube-style web game called “The Cube.” It sets up a user interface with sections for the game display, background, title, timer, and various settings like cube size, flip style, scramble length, camera view, and color themes. It also shows statistics such as best time and total solves, and includes buttons for settings, stats, theme switch, reset, and navigation. The functionality and 3D graphics are handled by external JavaScript (script.js) and Three.js library.

    <!DOCTYPE html>
    <html lang="en" >
    <head>
      <meta charset="UTF-8">
      <title>Animated rubik Cube</title>
      <meta name="viewport" content="width=device-width,height=device-height,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><link rel="stylesheet" href="./style.css">
    
    </head>
    <body>
    
    <div class="ui">
    
      <div class="ui__background"></div>
    
      <div class="ui__game"></div>
    
      <div class="ui__texts">
        <h1 class="text text--title">
          <span>THE</span>
          <span>CUBE</span>
        </h1>
        <div class="text text--note">
          Double tap to start
        </div>
        <div class="text text--timer">
          0:00
        </div>
        <div class="text text--complete">
          <span>Complete!</span>
        </div>
        <div class="text text--best-time">
          <icon trophy></icon>
          <span>Best Time!</span>
        </div>
      </div>
    
      <div class="ui__prefs">
        <range name="size" title="Cube Size" list="2,3,4,5"></range>
        <range name="flip" title="Flip Type" list="Swift&nbsp;,Smooth,Bounce"></range>
        <range name="scramble" title="Scramble Length" list="20,25,30"></range>
        <range name="fov" title="Camera Angle" list="Ortographic,Perspective"></range>
        <range name="theme" title="Color Scheme" list="Cube,Erno,Dust,Camo,Rain"></range>
      </div>
    
      <div class="ui__theme">
        <range name="hue" title="Hue" color></range>
        <range name="saturation" title="Saturation" color></range>
        <range name="lightness" title="Lightness" color></range>
      </div>
    
      <div class="ui__stats">
        <div class="stats" name="cube-size">
          <i>Cube:</i><b>3x3x3</b>
        </div>
        <div class="stats" name="total-solves">
          <i>Total solves:</i><b>-</b>
        </div>
        <div class="stats" name="best-time">
          <i>Best time:</i><b>-</b>
        </div>
        <div class="stats" name="worst-time">
          <i>Worst time:</i><b>-</b>
        </div>
        <div class="stats" name="average-5">
          <i>Average of 5:</i><b>-</b>
        </div>
        <div class="stats" name="average-12">
          <i>Average of 12:</i><b>-</b>
        </div>
        <div class="stats" name="average-25">
          <i>Average of 25:</i><b>-</b>
        </div>
      </div>
    
      <div class="ui__buttons">
        <button class="btn btn--bl btn--stats">
          <icon trophy></icon>
        </button>
        <button class="btn btn--br btn--prefs">
          <icon settings></icon>
        </button>
        <button class="btn btn--bl btn--back">
          <icon back></icon>
        </button>
        <button class="btn btn--br btn--theme">
          <icon theme></icon>
        </button>
        <button class="btn btn--br btn--reset">
          <icon reset></icon>
        </button>
      </div>
    
    </div>
    
      <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js'></script><script  src="./script.js"></script>
    
    </body>
    </html>
    

    CSS :

    This CSS styles a minimal, full-screen interactive UI—likely for a color or timer-based game. It uses a custom “BungeeFont”, resets all default styles, and disables user selection and scrolling. It defines components like sliders (.range), stats (.stats), overlay text (.text), and buttons (.btn), with responsive sizing and transitions. The .ui wrapper manages game layers like background, canvas, controls, and text overlays, ensuring a clean, immersive experience with no distractions.

    @font-face {
      font-family: "BungeeFont";
      font-weight: normal;
      font-style: normal;
      src: url("data:font/truetype;charset=utf-8;base64,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") format("woff2"), url("data:font/truetype;charset=utf-8;base64,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") format("woff");
    }
    *, *:before, *:after {
      -webkit-user-select: none;
      -moz-user-select: none;
      user-select: none;
      box-sizing: border-box;
      cursor: inherit;
      margin: 0;
      padding: 0;
      outline: none;
      font-size: inherit;
      font-family: inherit;
      font-weight: inherit;
      font-style: inherit;
      text-transform: uppercase;
    }
    *:focus {
      outline: none;
    }
    
    html {
      -webkit-tap-highlight-color: transparent;
      -webkit-text-size-adjust: 100%;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      -ms-text-size-adjust: 100%;
      -webkit-text-size-adjust: 100%;
      overflow: hidden;
      height: 100%;
    }
    
    body {
      font-family: "BungeeFont", sans-serif;
      font-weight: normal;
      font-style: normal;
      line-height: 1;
      cursor: default;
      overflow: hidden;
      height: 100%;
      font-size: 5rem;
    }
    
    .icon {
      display: inline-block;
      font-size: inherit;
      overflow: visible;
      vertical-align: -0.125em;
      preserveAspectRatio: none;
    }
    
    .range {
      position: relative;
      width: 14em;
      z-index: 1;
      opacity: 0;
    }
    .range:not(:last-child) {
      margin-bottom: 2em;
    }
    .range__label {
      position: relative;
      font-size: 0.9em;
      line-height: 0.75em;
      padding-bottom: 0.5em;
      z-index: 2;
    }
    .range__track {
      position: relative;
      height: 1em;
      margin-left: 0.5em;
      margin-right: 0.5em;
      z-index: 3;
    }
    .range__track-line {
      position: absolute;
      background: rgba(0, 0, 0, 0.2);
      height: 2px;
      top: 50%;
      margin-top: -1px;
      left: -0.5em;
      right: -0.5em;
      transform-origin: left center;
    }
    .range__handle {
      position: absolute;
      width: 0;
      height: 0;
      top: 50%;
      left: 0;
      cursor: pointer;
      z-index: 1;
    }
    .range__handle div {
      transition: background 500ms ease;
      position: absolute;
      left: 0;
      top: 0;
      width: 0.9em;
      height: 0.9em;
      border-radius: 0.2em;
      margin-left: -0.45em;
      margin-top: -0.45em;
      background: #41aac8;
      border-bottom: 2px solid rgba(0, 0, 0, 0.2);
    }
    .range.is-active .range__handle div {
      transform: scale(1.25);
    }
    .range__handle:after {
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      width: 3em;
      height: 3em;
      margin-left: -1.5em;
      margin-top: -1.5em;
    }
    .range__list {
      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      position: relative;
      padding-top: 0.5em;
      font-size: 0.55em;
      color: rgba(0, 0, 0, 0.5);
      z-index: 1;
    }
    .range--type-color:not(:last-child) {
      margin-bottom: 1em;
    }
    .range--type-color .range__list {
      display: none;
    }
    .range--type-color .range__handle > div {
      background: currentColor !important;
    }
    .range--type-color .range__track-line {
      background: transparent;
    }
    .range--type-color .range__track-line:after {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      content: "";
      opacity: 0.5;
    }
    .range--color-hue .range__handle {
      color: red;
    }
    .range--color-hue .range__track {
      color: red;
    }
    .range--color-hue .range__track-line:after {
      background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
    }
    .range--color-saturation .range__handle {
      color: red;
    }
    .range--color-saturation .range__track {
      color: red;
    }
    .range--color-saturation .range__track-line:after {
      background: linear-gradient(to right, gray, currentColor);
    }
    .range--color-lightness .range__handle {
      color: red;
    }
    .range--color-lightness .range__track {
      color: red;
    }
    .range--color-lightness .range__track-line:after {
      background: linear-gradient(to right, black, currentColor, white);
    }
    
    .stats {
      position: relative;
      width: 14em;
      z-index: 1;
      display: flex;
      justify-content: space-between;
      opacity: 0;
    }
    .stats:not(:last-child) {
      margin-bottom: 1.5em;
    }
    .stats > i {
      display: block;
      color: rgba(0, 0, 0, 0.5);
      font-size: 0.9em;
    }
    .stats > b {
      display: block;
      font-size: 0.9em;
    }
    .stats > b > i {
      font-size: 0.75em;
    }
    .stats[name=worst-time] {
      display: none;
    }
    
    .text {
      position: absolute;
      left: 0;
      right: 0;
      text-align: center;
      line-height: 0.75;
      perspective: 100rem;
      opacity: 0;
    }
    .text i {
      display: inline-block;
      opacity: 0;
      white-space: pre-wrap;
    }
    .text--title {
      bottom: 75%;
      font-size: 4.4em;
      height: 1.2em;
    }
    .text--title span {
      display: block;
    }
    .text--title span:first-child {
      font-size: 0.5em;
      margin-bottom: 0.2em;
    }
    .text--note {
      top: 87%;
      font-size: 1em;
    }
    .text--timer {
      bottom: 78%;
      font-size: 3.5em;
      line-height: 1;
    }
    .text--complete, .text--best-time {
      font-size: 1.5em;
      top: 83%;
      line-height: 1em;
    }
    
    .btn {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
      background-color: transparent;
      border-radius: 0;
      border-width: 0;
      position: absolute;
      pointer-events: none;
      font-size: 1.2em;
      color: rgba(0, 0, 0, 0.25);
      opacity: 0;
    }
    .btn:after {
      position: absolute;
      content: "";
      width: 3em;
      height: 3em;
      left: 50%;
      top: 50%;
      margin-left: -1.5em;
      margin-top: -1.5em;
      border-radius: 100%;
    }
    .btn--bl {
      bottom: 0.8em;
      left: 0.8em;
    }
    .btn--br {
      bottom: 0.8em;
      right: 0.8em;
    }
    .btn--bc {
      bottom: 0.8em;
      left: calc(50% - 0.5em);
    }
    .btn svg {
      display: block;
    }
    .btn--cancel {
      display: none !important;
    }
    
    .ui {
      pointer-events: none;
      color: #070d15;
    }
    .ui, .ui__background, .ui__game, .ui__texts, .ui__prefs, .ui__theme, .ui__stats, .ui__buttons {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
    .ui__background {
      z-index: 1;
      transition: background 500ms ease;
      background: #d1d5db;
    }
    .ui__background:after {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      content: "";
      background-image: linear-gradient(to bottom, white 50%, rgba(255, 255, 255, 0) 100%);
    }
    .ui__game {
      pointer-events: all;
      z-index: 2;
    }
    .ui__game canvas {
      display: block;
      width: 100%;
      height: 100%;
    }
    .ui__texts {
      z-index: 3;
    }
    .ui__prefs, .ui__stats, .ui__theme {
      display: flex;
      flex-flow: column nowrap;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      z-index: 4;
    }
    .ui__theme {
      padding-top: 15em;
    }
    .ui__buttons {
      z-index: 5;
    }

    JavaScript:

    This code sets up a lightweight animation system using JavaScript and Three.js. The animationEngine manages and updates animations via requestAnimationFrame, tracking each with a unique ID. Animation is a base class with start, stop, and update methods. World extends Animation to create a Three.js scene with camera, renderer, lights, and responsive resizing. It ensures consistent 3D rendering across screen sizes. Additionally, RoundedBoxGeometry is a custom geometry generator that creates smooth, rounded cube-like meshes by calculating vertices, normals, and indices for proper shading and structure.

    const animationEngine = ( () => {
    
      let uniqueID = 0;
    
      class AnimationEngine {
    
        constructor() {
    
          this.ids = [];
          this.animations = {};
          this.update = this.update.bind( this );
          this.raf = 0;
          this.time = 0;
    
        }
    
        update() {
    
          const now = performance.now();
          const delta = now - this.time;
          this.time = now;
    
          let i = this.ids.length;
    
          this.raf = i ? requestAnimationFrame( this.update ) : 0;
    
          while ( i-- )
            this.animations[ this.ids[ i ] ] && this.animations[ this.ids[ i ] ].update( delta );
    
        }
    
        add( animation ) {
    
          animation.id = uniqueID ++;
    
          this.ids.push( animation.id );
          this.animations[ animation.id ] = animation;
    
          if ( this.raf !== 0 ) return;
    
          this.time = performance.now();
          this.raf = requestAnimationFrame( this.update );
    
        }
    
        remove( animation ) {
    
          const index = this.ids.indexOf( animation.id );
    
          if ( index < 0 ) return;
    
          this.ids.splice( index, 1 );
          delete this.animations[ animation.id ];
          animation = null;
    
        }
    
      }
    
      return new AnimationEngine();
    
    } )();
    
    class Animation {
    
      constructor( start ) {
    
        if ( start === true ) this.start();
    
      }
    
      start() {
    
        animationEngine.add( this );
    
      }
    
      stop() {
    
        animationEngine.remove( this );
    
      }
    
      update( delta ) {}
    
    }
    
    class World extends Animation {
    
      constructor( game ) {
    
        super( true );
    
        this.game = game;
    
        this.container = this.game.dom.game;
        this.scene = new THREE.Scene();
    
        this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
        this.renderer.setPixelRatio( window.devicePixelRatio );
        this.container.appendChild( this.renderer.domElement );
    
        this.camera = new THREE.PerspectiveCamera( 2, 1, 0.1, 10000 );
    
        this.stage = { width: 2, height: 3 };
        this.fov = 10;
    
        this.createLights();
    
        this.onResize = [];
    
        this.resize();
        window.addEventListener( 'resize', () => this.resize(), false );
    
      }
    
      update() {
    
        this.renderer.render( this.scene, this.camera );
    
      }
    
      resize() {
    
        this.width = this.container.offsetWidth;
        this.height = this.container.offsetHeight;
    
        this.renderer.setSize( this.width, this.height );
    
        this.camera.fov = this.fov;
        this.camera.aspect = this.width / this.height;
    
        const aspect = this.stage.width / this.stage.height;
        const fovRad = this.fov * THREE.Math.DEG2RAD;
    
        let distance = ( aspect < this.camera.aspect )
          ? ( this.stage.height / 2 ) / Math.tan( fovRad / 2 )
          : ( this.stage.width / this.camera.aspect ) / ( 2 * Math.tan( fovRad / 2 ) );
    
        distance *= 0.5;
    
        this.camera.position.set( distance, distance, distance);
        this.camera.lookAt( this.scene.position );
        this.camera.updateProjectionMatrix();
    
        const docFontSize = ( aspect < this.camera.aspect )
          ? ( this.height / 100 ) * aspect
          : this.width / 100;
    
        document.documentElement.style.fontSize = docFontSize + 'px';
    
        if ( this.onResize ) this.onResize.forEach( cb => cb() );
    
      }
    
      createLights() {
    
        this.lights = {
          holder:  new THREE.Object3D,
          ambient: new THREE.AmbientLight( 0xffffff, 0.69 ),
          front:   new THREE.DirectionalLight( 0xffffff, 0.36 ),
          back:    new THREE.DirectionalLight( 0xffffff, 0.19 ),
        };
    
        this.lights.front.position.set( 1.5, 5, 3 );
        this.lights.back.position.set( -1.5, -5, -3 );
    
        this.lights.holder.add( this.lights.ambient );
        this.lights.holder.add( this.lights.front );
        this.lights.holder.add( this.lights.back );
    
        this.scene.add( this.lights.holder );
    
      }
    
    }
    
    function RoundedBoxGeometry( size, radius, radiusSegments ) {
    
      THREE.BufferGeometry.call( this );
    
      this.type = 'RoundedBoxGeometry';
    
      radiusSegments = ! isNaN( radiusSegments ) ? Math.max( 1, Math.floor( radiusSegments ) ) : 1;
    
      var width, height, depth;
    
      width = height = depth = size;
      radius = size * radius;
    
      radius = Math.min( radius, Math.min( width, Math.min( height, Math.min( depth ) ) ) / 2 );
    
      var edgeHalfWidth = width / 2 - radius;
      var edgeHalfHeight = height / 2 - radius;
      var edgeHalfDepth = depth / 2 - radius;
    
      this.parameters = {
        width: width,
        height: height,
        depth: depth,
        radius: radius,
        radiusSegments: radiusSegments
      };
    
      var rs1 = radiusSegments + 1;
      var totalVertexCount = ( rs1 * radiusSegments + 1 ) << 3;
    
      var positions = new THREE.BufferAttribute( new Float32Array( totalVertexCount * 3 ), 3 );
      var normals = new THREE.BufferAttribute( new Float32Array( totalVertexCount * 3 ), 3 );
    
      var
        cornerVerts = [],
        cornerNormals = [],
        normal = new THREE.Vector3(),
        vertex = new THREE.Vector3(),
        vertexPool = [],
        normalPool = [],
        indices = []
      ;
    
      var
        lastVertex = rs1 * radiusSegments,
        cornerVertNumber = rs1 * radiusSegments + 1
      ;
    
      doVertices();
      doFaces();
      doCorners();
      doHeightEdges();
      doWidthEdges();
      doDepthEdges();
    
      function doVertices() {
    
        var cornerLayout = [
          new THREE.Vector3( 1, 1, 1 ),
          new THREE.Vector3( 1, 1, - 1 ),
          new THREE.Vector3( - 1, 1, - 1 ),
          new THREE.Vector3( - 1, 1, 1 ),
          new THREE.Vector3( 1, - 1, 1 ),
          new THREE.Vector3( 1, - 1, - 1 ),
          new THREE.Vector3( - 1, - 1, - 1 ),
          new THREE.Vector3( - 1, - 1, 1 )
        ];
    
        for ( var j = 0; j < 8; j ++ ) {
    
          cornerVerts.push( [] );
          cornerNormals.push( [] );
    
        }
    
        var PIhalf = Math.PI / 2;
        var cornerOffset = new THREE.Vector3( edgeHalfWidth, edgeHalfHeight, edgeHalfDepth );
    
        for ( var y = 0; y <= radiusSegments; y ++ ) {
    
          var v = y / radiusSegments;
          var va = v * PIhalf;
          var cosVa = Math.cos( va );
          var sinVa = Math.sin( va );
    
          if ( y == radiusSegments ) {
    
            vertex.set( 0, 1, 0 );
            var vert = vertex.clone().multiplyScalar( radius ).add( cornerOffset );
            cornerVerts[ 0 ].push( vert );
            vertexPool.push( vert );
            var norm = vertex.clone();
            cornerNormals[ 0 ].push( norm );
            normalPool.push( norm );
            continue;
    
          }
    
          for ( var x = 0; x <= radiusSegments; x ++ ) {
    
            var u = x / radiusSegments;
            var ha = u * PIhalf;
            vertex.x = cosVa * Math.cos( ha );
            vertex.y = sinVa;
            vertex.z = cosVa * Math.sin( ha );
    
            var vert = vertex.clone().multiplyScalar( radius ).add( cornerOffset );
            cornerVerts[ 0 ].push( vert );
            vertexPool.push( vert );
    
            var norm = vertex.clone().normalize();
            cornerNormals[ 0 ].push( norm );
            normalPool.push( norm );
    
          }
    
        }
    
        for ( var i = 1; i < 8; i ++ ) {
    
          for ( var j = 0; j < cornerVerts[ 0 ].length; j ++ ) {
    
            var vert = cornerVerts[ 0 ][ j ].clone().multiply( cornerLayout[ i ] );
            cornerVerts[ i ].push( vert );
            vertexPool.push( vert );
    
            var norm = cornerNormals[ 0 ][ j ].clone().multiply( cornerLayout[ i ] );
            cornerNormals[ i ].push( norm );
            normalPool.push( norm );
    
          }
    
        }
    
      }
    
      function doCorners() {
    
        var flips = [
          true,
          false,
          true,
          false,
          false,
          true,
          false,
          true
        ];
    
        var lastRowOffset = rs1 * ( radiusSegments - 1 );
    
        for ( var i = 0; i < 8; i ++ ) {
    
          var cornerOffset = cornerVertNumber * i;
    
          for ( var v = 0; v < radiusSegments - 1; v ++ ) {
    
            var r1 = v * rs1;
            var r2 = ( v + 1 ) * rs1;
    
            for ( var u = 0; u < radiusSegments; u ++ ) {
    
              var u1 = u + 1;
              var a = cornerOffset + r1 + u;
              var b = cornerOffset + r1 + u1;
              var c = cornerOffset + r2 + u;
              var d = cornerOffset + r2 + u1;
    
              if ( ! flips[ i ] ) {
    
                indices.push( a );
                indices.push( b );
                indices.push( c );
    
                indices.push( b );
                indices.push( d );
                indices.push( c );
    
              } else {
    
                indices.push( a );
                indices.push( c );
                indices.push( b );
    
                indices.push( b );
                indices.push( c );
                indices.push( d );
    
              }
    
            }
    
          }
    
          for ( var u = 0; u < radiusSegments; u ++ ) {
    
            var a = cornerOffset + lastRowOffset + u;
            var b = cornerOffset + lastRowOffset + u + 1;
            var c = cornerOffset + lastVertex;
    
            if ( ! flips[ i ] ) {
    
              indices.push( a );
              indices.push( b );
              indices.push( c );
    
            } else {
    
              indices.push( a );
              indices.push( c );
              indices.push( b );
    
            }
    
          }
    
        }
    
      }
    
      function doFaces() {
    
        var a = lastVertex;
        var b = lastVertex + cornerVertNumber;
        var c = lastVertex + cornerVertNumber * 2;
        var d = lastVertex + cornerVertNumber * 3;
    
        indices.push( a );
        indices.push( b );
        indices.push( c );
        indices.push( a );
        indices.push( c );
        indices.push( d );
    
        a = lastVertex + cornerVertNumber * 4;
        b = lastVertex + cornerVertNumber * 5;
        c = lastVertex + cornerVertNumber * 6;
        d = lastVertex + cornerVertNumber * 7;
    
        indices.push( a );
        indices.push( c );
        indices.push( b );
        indices.push( a );
        indices.push( d );
        indices.push( c );
    
        a = 0;
        b = cornerVertNumber;
        c = cornerVertNumber * 4;
        d = cornerVertNumber * 5;
    
        indices.push( a );
        indices.push( c );
        indices.push( b );
        indices.push( b );
        indices.push( c );
        indices.push( d );
    
        a = cornerVertNumber * 2;
        b = cornerVertNumber * 3;
        c = cornerVertNumber * 6;
        d = cornerVertNumber * 7;
    
        indices.push( a );
        indices.push( c );
        indices.push( b );
        indices.push( b );
        indices.push( c );
        indices.push( d );
    
        a = radiusSegments;
        b = radiusSegments + cornerVertNumber * 3;
        c = radiusSegments + cornerVertNumber * 4;
        d = radiusSegments + cornerVertNumber * 7;
    
        indices.push( a );
        indices.push( b );
        indices.push( c );
        indices.push( b );
        indices.push( d );
        indices.push( c );
    
        a = radiusSegments + cornerVertNumber;
        b = radiusSegments + cornerVertNumber * 2;
        c = radiusSegments + cornerVertNumber * 5;
        d = radiusSegments + cornerVertNumber * 6;
    
        indices.push( a );
        indices.push( c );
        indices.push( b );
        indices.push( b );
        indices.push( c );
        indices.push( d );
    
      }
    
      function doHeightEdges() {
    
        for ( var i = 0; i < 4; i ++ ) {
    
          var cOffset = i * cornerVertNumber;
          var cRowOffset = 4 * cornerVertNumber + cOffset;
          var needsFlip = i & 1 === 1;
    
          for ( var u = 0; u < radiusSegments; u ++ ) {
    
            var u1 = u + 1;
            var a = cOffset + u;
            var b = cOffset + u1;
            var c = cRowOffset + u;
            var d = cRowOffset + u1;
    
            if ( ! needsFlip ) {
    
              indices.push( a );
              indices.push( b );
              indices.push( c );
              indices.push( b );
              indices.push( d );
              indices.push( c );
    
            } else {
    
              indices.push( a );
              indices.push( c );
              indices.push( b );
              indices.push( b );
              indices.push( c );
              indices.push( d );
    
            }
    
          }
    
        }
    
      }
    
      function doDepthEdges() {
    
        var cStarts = [ 0, 2, 4, 6 ];
        var cEnds = [ 1, 3, 5, 7 ];
    
        for ( var i = 0; i < 4; i ++ ) {
    
          var cStart = cornerVertNumber * cStarts[ i ];
          var cEnd = cornerVertNumber * cEnds[ i ];
    
          var needsFlip = 1 >= i;
    
          for ( var u = 0; u < radiusSegments; u ++ ) {
    
            var urs1 = u * rs1;
            var u1rs1 = ( u + 1 ) * rs1;
    
            var a = cStart + urs1;
            var b = cStart + u1rs1;
            var c = cEnd + urs1;
            var d = cEnd + u1rs1;
    
            if ( needsFlip ) {
    
              indices.push( a );
              indices.push( c );
              indices.push( b );
              indices.push( b );
              indices.push( c );
              indices.push( d );
    
            } else {
    
              indices.push( a );
              indices.push( b );
              indices.push( c );
              indices.push( b );
              indices.push( d );
              indices.push( c );
    
            }
    
          }
    
        }
    
      }
    
      function doWidthEdges() {
    
        var end = radiusSegments - 1;
    
        var cStarts = [ 0, 1, 4, 5 ];
        var cEnds = [ 3, 2, 7, 6 ];
        var needsFlip = [ 0, 1, 1, 0 ];
    
        for ( var i = 0; i < 4; i ++ ) {
    
          var cStart = cStarts[ i ] * cornerVertNumber;
          var cEnd = cEnds[ i ] * cornerVertNumber;
    
          for ( var u = 0; u <= end; u ++ ) {
    
            var a = cStart + radiusSegments + u * rs1;
            var b = cStart + ( u != end ? radiusSegments + ( u + 1 ) * rs1 : cornerVertNumber - 1 );
    
            var c = cEnd + radiusSegments + u * rs1;
            var d = cEnd + ( u != end ? radiusSegments + ( u + 1 ) * rs1 : cornerVertNumber - 1 );
    
            if ( ! needsFlip[ i ] ) {
    
              indices.push( a );
              indices.push( b );
              indices.push( c );
              indices.push( b );
              indices.push( d );
              indices.push( c );
    
            } else {
    
              indices.push( a );
              indices.push( c );
              indices.push( b );
              indices.push( b );
              indices.push( c );
              indices.push( d );
    
            }
    
          }
    
        }
    
      }
    
      var index = 0;
    
      for ( var i = 0; i < vertexPool.length; i ++ ) {
    
        positions.setXYZ(
          index,
          vertexPool[ i ].x,
          vertexPool[ i ].y,
          vertexPool[ i ].z
        );
    
        normals.setXYZ(
          index,
          normalPool[ i ].x,
          normalPool[ i ].y,
          normalPool[ i ].z
        );
    
        index ++;
    
      }
    
      this.setIndex( new THREE.BufferAttribute( new Uint16Array( indices ), 1 ) );
      this.addAttribute( 'position', positions );
      this.addAttribute( 'normal', normals );
    
    }
    
    RoundedBoxGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
    RoundedBoxGeometry.constructor = RoundedBoxGeometry;
    
    function RoundedPlaneGeometry( size, radius, depth ) {
    
      var x, y, width, height;
    
      x = y = - size / 2;
      width = height = size;
      radius = size * radius;
    
      const shape = new THREE.Shape();
    
      shape.moveTo( x, y + radius );
      shape.lineTo( x, y + height - radius );
      shape.quadraticCurveTo( x, y + height, x + radius, y + height );
      shape.lineTo( x + width - radius, y + height );
      shape.quadraticCurveTo( x + width, y + height, x + width, y + height - radius );
      shape.lineTo( x + width, y + radius );
      shape.quadraticCurveTo( x + width, y, x + width - radius, y );
      shape.lineTo( x + radius, y );
      shape.quadraticCurveTo( x, y, x, y + radius );
    
      const geometry = new THREE.ExtrudeBufferGeometry(
        shape,
        { depth: depth, bevelEnabled: false, curveSegments: 3 }
      );
    
      return geometry;
    
    }
    
    class Cube {
    
      constructor( game ) {
    
        this.game = game;
        this.size = 3;
    
        this.geometry = {
          pieceCornerRadius: 0.12,
          edgeCornerRoundness: 0.15,
          edgeScale: 0.82,
          edgeDepth: 0.01,
        };
    
        this.holder = new THREE.Object3D();
        this.object = new THREE.Object3D();
        this.animator = new THREE.Object3D();
    
        this.holder.add( this.animator );
        this.animator.add( this.object );
    
        this.game.world.scene.add( this.holder );
    
      }
    
      init() {
    
        this.cubes = [];
        this.object.children = [];
        this.object.add( this.game.controls.group );
    
        if ( this.size === 2 ) this.scale = 1.25;
        else if ( this.size === 3 ) this.scale = 1;
        else if ( this.size > 3 ) this.scale = 3 / this.size;
    
        this.object.scale.set( this.scale, this.scale, this.scale );
    
        const controlsScale = this.size === 2 ? 0.825 : 1;
        this.game.controls.edges.scale.set( controlsScale, controlsScale, controlsScale );
        
        this.generatePositions();
        this.generateModel();
    
        this.pieces.forEach( piece => {
    
          this.cubes.push( piece.userData.cube );
          this.object.add( piece );
    
        } );
    
        this.holder.traverse( node => {
    
          if ( node.frustumCulled ) node.frustumCulled = false;
    
        } );
    
        this.updateColors( this.game.themes.getColors() );
    
        this.sizeGenerated = this.size;
    
      }
    
      resize( force = false ) {
    
        if ( this.size !== this.sizeGenerated || force ) {
    
          this.size = this.game.preferences.ranges.size.value;
    
          this.reset();
          this.init();
    
          this.game.saved = false;
          this.game.timer.reset();
          this.game.storage.clearGame();
    
        }
    
      }
    
      reset() {
    
        this.game.controls.edges.rotation.set( 0, 0, 0 );
    
        this.holder.rotation.set( 0, 0, 0 );
        this.object.rotation.set( 0, 0, 0 );
        this.animator.rotation.set( 0, 0, 0 );
    
      }
    
      generatePositions() {
    
        const m = this.size - 1;
        const first = this.size % 2 !== 0
          ? 0 - Math.floor(this.size / 2)
          : 0.5 - this.size / 2;
    
        let x, y, z;
    
        this.positions = [];
    
        for ( x = 0; x < this.size; x ++ ) {
          for ( y = 0; y < this.size; y ++ ) {
            for ( z = 0; z < this.size; z ++ ) {
    
              let position = new THREE.Vector3(first + x, first + y, first + z);
              let edges = [];
    
              if ( x == 0 ) edges.push(0);
              if ( x == m ) edges.push(1);
              if ( y == 0 ) edges.push(2);
              if ( y == m ) edges.push(3);
              if ( z == 0 ) edges.push(4);
              if ( z == m ) edges.push(5);
    
              position.edges = edges;
              this.positions.push( position );
    
            }
          }
        }
    
      }
    
      generateModel() {
    
        this.pieces = [];
        this.edges = [];
    
        const pieceSize = 1 / 3;
    
        const mainMaterial = new THREE.MeshLambertMaterial();
    
        const pieceMesh = new THREE.Mesh(
          new RoundedBoxGeometry( pieceSize, this.geometry.pieceCornerRadius, 3 ),
          mainMaterial.clone()
        );
    
        const edgeGeometry = RoundedPlaneGeometry(
          pieceSize,
          this.geometry.edgeCornerRoundness,
          this.geometry.edgeDepth
        );
    
        this.positions.forEach( ( position, index ) => {
    
          const piece = new THREE.Object3D();
          const pieceCube = pieceMesh.clone();
          const pieceEdges = [];
    
          piece.position.copy( position.clone().divideScalar( 3 ) );
          piece.add( pieceCube );
          piece.name = index;
          piece.edgesName = '';
    
          position.edges.forEach( position => {
    
            const edge = new THREE.Mesh( edgeGeometry, mainMaterial.clone() );
            const name = [ 'L', 'R', 'D', 'U', 'B', 'F' ][ position ];
            const distance = pieceSize / 2;
    
            edge.position.set(
              distance * [ - 1, 1, 0, 0, 0, 0 ][ position ],
              distance * [ 0, 0, - 1, 1, 0, 0 ][ position ],
              distance * [ 0, 0, 0, 0, - 1, 1 ][ position ]
            );
    
            edge.rotation.set(
              Math.PI / 2 * [ 0, 0, 1, - 1, 0, 0 ][ position ],
              Math.PI / 2 * [ - 1, 1, 0, 0, 2, 0 ][ position ],
              0
            );
    
            edge.scale.set(
              this.geometry.edgeScale,
              this.geometry.edgeScale,
              this.geometry.edgeScale
            );
    
            edge.name = name;
    
            piece.add( edge );
            pieceEdges.push( name );
            this.edges.push( edge );
    
          } );
    
          piece.userData.edges = pieceEdges;
          piece.userData.cube = pieceCube;
    
          piece.userData.start = {
            position: piece.position.clone(),
            rotation: piece.rotation.clone(),
          };
    
          this.pieces.push( piece );
    
        } );
    
      }
    
      updateColors( colors ) {
    
        if ( typeof this.pieces !== 'object' && typeof this.edges !== 'object' ) return;
    
        this.pieces.forEach( piece => piece.userData.cube.material.color.setHex( colors.P ) );
        this.edges.forEach( edge => edge.material.color.setHex( colors[ edge.name ] ) );
    
      }
    
      loadFromData( data ) {
    
        this.size = data.size;
    
        this.reset();
        this.init();
    
        this.pieces.forEach( piece => {
    
          const index = data.names.indexOf( piece.name );
    
          const position = data.positions[index];
          const rotation = data.rotations[index];
    
          piece.position.set( position.x, position.y, position.z );
          piece.rotation.set( rotation.x, rotation.y, rotation.z );
    
        } );
    
      }
    
    }
    
    const Easing = {
    
      Power: {
    
        In: power => {
    
          power = Math.round( power || 1 );
    
          return t => Math.pow( t, power );
    
        },
    
        Out: power => {
    
          power = Math.round( power || 1 );
    
          return t => 1 - Math.abs( Math.pow( t - 1, power ) );
    
        },
    
        InOut: power => {
    
          power = Math.round( power || 1 );
    
          return t => ( t < 0.5 )
            ? Math.pow( t * 2, power ) / 2
            : ( 1 - Math.abs( Math.pow( ( t * 2 - 1 ) - 1, power ) ) ) / 2 + 0.5;
    
        },
    
      },
    
      Sine: {
    
        In: () => t => 1 + Math.sin( Math.PI / 2 * t - Math.PI / 2 ),
    
        Out: () => t => Math.sin( Math.PI / 2 * t ),
    
        InOut: () => t => ( 1 + Math.sin( Math.PI * t - Math.PI / 2 ) ) / 2,
    
      },
    
      Back: {
    
        Out: s => {
    
          s = s || 1.70158;
    
          return t => { return ( t -= 1 ) * t * ( ( s + 1 ) * t + s ) + 1; };
    
        },
    
        In: s => {
    
          s = s || 1.70158;
    
          return t => { return t * t * ( ( s + 1 ) * t - s ); };
    
        }
    
      },
    
      Elastic: {
    
        Out: ( amplitude, period ) => {
    
          let PI2 = Math.PI * 2;
    
          let p1 = ( amplitude >= 1 ) ? amplitude : 1;
          let p2 = ( period || 0.3 ) / ( amplitude < 1 ? amplitude : 1 );
          let p3 = p2 / PI2 * ( Math.asin( 1 / p1 ) || 0 );
    
          p2 = PI2 / p2;
    
          return t => { return p1 * Math.pow( 2, -10 * t ) * Math.sin( ( t - p3 ) * p2 ) + 1 }
    
        },
    
      },
    
    };
    
    class Tween extends Animation {
    
      constructor( options ) {
    
        super( false );
    
        this.duration = options.duration || 500;
        this.easing = options.easing || ( t => t );
        this.onUpdate = options.onUpdate || ( () => {} );
        this.onComplete = options.onComplete || ( () => {} );
    
        this.delay = options.delay || false;
        this.yoyo = options.yoyo ? false : null;
    
        this.progress = 0;
        this.value = 0;
        this.delta = 0;
    
        this.getFromTo( options );
    
        if ( this.delay ) setTimeout( () => super.start(), this.delay );
        else super.start();
    
        this.onUpdate( this );
    
      }
    
      update( delta ) {
    
        const old = this.value * 1;
        const direction = ( this.yoyo === true ) ? - 1 : 1;
    
        this.progress += ( delta / this.duration ) * direction;
    
        this.value = this.easing( this.progress );
        this.delta = this.value - old;
    
        if ( this.values !== null ) this.updateFromTo();
    
        if ( this.yoyo !== null ) this.updateYoyo();
        else if ( this.progress <= 1 ) this.onUpdate( this );
        else {
    
          this.progress = 1;
          this.value = 1;
          this.onUpdate( this );
          this.onComplete( this );
          super.stop();      
    
        }
    
      }
    
      updateYoyo() {
    
        if ( this.progress > 1 || this.progress < 0 ) {
    
          this.value = this.progress = ( this.progress > 1 ) ? 1 : 0;
          this.yoyo = ! this.yoyo;
    
        }
    
        this.onUpdate( this );
    
      }
    
      updateFromTo() {
    
        this.values.forEach( key => {
    
          this.target[ key ] = this.from[ key ] + ( this.to[ key ] - this.from[ key ] ) * this.value;
    
        } );
    
      }
    
      getFromTo( options ) {
    
        if ( ! options.target || ! options.to ) {
    
          this.values = null;
          return;
    
        }
    
        this.target = options.target || null;
        this.from = options.from || {};
        this.to = options.to || null;
        this.values = [];
    
        if ( Object.keys( this.from ).length < 1 )
          Object.keys( this.to ).forEach( key => { this.from[ key ] = this.target[ key ]; } );
    
        Object.keys( this.to ).forEach( key => { this.values.push( key ); } );
    
      }
    
    }
    
    window.addEventListener( 'touchmove', () => {} );
    document.addEventListener( 'touchmove',  event => { event.preventDefault(); }, { passive: false } );
    
    class Draggable {
    
      constructor( element, options ) {
    
        this.position = {
          current: new THREE.Vector2(),
          start: new THREE.Vector2(),
          delta: new THREE.Vector2(),
          old: new THREE.Vector2(),
          drag: new THREE.Vector2(),
        };
    
        this.options = Object.assign( {
          calcDelta: false,
        }, options || {} );
    
        this.element = element;
        this.touch = null;
    
        this.drag = {
    
          start: ( event ) => {
    
            if ( event.type == 'mousedown' && event.which != 1 ) return;
            if ( event.type == 'touchstart' && event.touches.length > 1 ) return;
    
            this.getPositionCurrent( event );
    
            if ( this.options.calcDelta ) {
    
              this.position.start = this.position.current.clone();
              this.position.delta.set( 0, 0 );
              this.position.drag.set( 0, 0 );
    
            }
    
            this.touch = ( event.type == 'touchstart' );
    
            this.onDragStart( this.position );
    
            window.addEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
            window.addEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );
    
          },
    
          move: ( event ) => {
    
            if ( this.options.calcDelta ) {
    
              this.position.old = this.position.current.clone();
    
            }
    
            this.getPositionCurrent( event );
    
            if ( this.options.calcDelta ) {
    
              this.position.delta = this.position.current.clone().sub( this.position.old );
              this.position.drag = this.position.current.clone().sub( this.position.start );
    
            }
    
            this.onDragMove( this.position );
    
          },
    
          end: ( event ) => {
    
            this.getPositionCurrent( event );
    
            this.onDragEnd( this.position );
    
            window.removeEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
            window.removeEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );
    
          },
    
        };
    
        this.onDragStart = () => {};
        this.onDragMove = () => {};
        this.onDragEnd = () => {};
    
        this.enable();
    
        return this;
    
      }
    
      enable() {
    
        this.element.addEventListener( 'touchstart', this.drag.start, false );
        this.element.addEventListener( 'mousedown', this.drag.start, false );
    
        return this;
    
      }
    
      disable() {
    
        this.element.removeEventListener( 'touchstart', this.drag.start, false );
        this.element.removeEventListener( 'mousedown', this.drag.start, false );
    
        return this;
    
      }
    
      getPositionCurrent( event ) {
    
        const dragEvent = event.touches
          ? ( event.touches[ 0 ] || event.changedTouches[ 0 ] )
          : event;
    
        this.position.current.set( dragEvent.pageX, dragEvent.pageY );
    
      }
    
      convertPosition( position ) {
    
        position.x = ( position.x / this.element.offsetWidth ) * 2 - 1;
        position.y = - ( ( position.y / this.element.offsetHeight ) * 2 - 1 );
    
        return position;
    
      }
    
    }
    
    const STILL = 0;
    const PREPARING = 1;
    const ROTATING = 2;
    const ANIMATING = 3;
    
    class Controls {
    
      constructor( game ) {
    
        this.game = game;
    
        this.flipConfig = 0;
    
        this.flipEasings = [ Easing.Power.Out( 3 ), Easing.Sine.Out(), Easing.Back.Out( 1.5 ) ];
        this.flipSpeeds = [ 125, 200, 300 ];
    
        this.raycaster = new THREE.Raycaster();
    
        const helperMaterial = new THREE.MeshBasicMaterial( { depthWrite: false, transparent: true, opacity: 0, color: 0x0033ff } );
    
        this.group = new THREE.Object3D();
        this.group.name = 'controls';
        this.game.cube.object.add( this.group );
    
        this.helper = new THREE.Mesh(
          new THREE.PlaneBufferGeometry( 200, 200 ),
          helperMaterial.clone()
        );
    
        this.helper.rotation.set( 0, Math.PI / 4, 0 );
        this.game.world.scene.add( this.helper );
    
        this.edges = new THREE.Mesh(
          new THREE.BoxBufferGeometry( 1, 1, 1 ),
          helperMaterial.clone(),
        );
    
        this.game.world.scene.add( this.edges );
    
        this.onSolved = () => {};
        this.onMove = () => {};
    
        this.momentum = [];
    
        this.scramble = null;
        this.state = STILL;
        this.enabled = false;
    
        this.initDraggable();
    
      }
    
      enable() {
    
        this.draggable.enable();
        this.enabled = true;
    
      }
    
      disable() {
    
        this.draggable.disable();
        this.enabled = false;
    
      }
    
      initDraggable() {
    
        this.draggable = new Draggable( this.game.dom.game );
    
        this.draggable.onDragStart = position => {
    
          if ( this.scramble !== null ) return;
          if ( this.state === PREPARING || this.state === ROTATING ) return;
    
          this.gettingDrag = this.state === ANIMATING;
    
          const edgeIntersect = this.getIntersect( position.current, this.edges, false );
    
          if ( edgeIntersect !== false ) {
    
            this.dragIntersect = this.getIntersect( position.current, this.game.cube.cubes, true );
    
          }
    
          if ( edgeIntersect !== false && this.dragIntersect !== false ) {
    
            this.dragNormal = edgeIntersect.face.normal.round();
            this.flipType = 'layer';
    
            this.attach( this.helper, this.edges );
    
            this.helper.rotation.set( 0, 0, 0 );
            this.helper.position.set( 0, 0, 0 );
            this.helper.lookAt( this.dragNormal );
            this.helper.translateZ( 0.5 );
            this.helper.updateMatrixWorld();
    
            this.detach( this.helper, this.edges );
    
          } else {
    
            this.dragNormal = new THREE.Vector3( 0, 0, 1 );
            this.flipType = 'cube';
    
            this.helper.position.set( 0, 0, 0 );
            this.helper.rotation.set( 0, Math.PI / 4, 0 );
            this.helper.updateMatrixWorld();
    
          }
    
          let planeIntersect = this.getIntersect( position.current, this.helper, false );
          if ( planeIntersect === false ) return;
    
          this.dragCurrent = this.helper.worldToLocal( planeIntersect.point );
          this.dragTotal = new THREE.Vector3();
          this.state = ( this.state === STILL ) ? PREPARING : this.state;
    
        };
    
        this.draggable.onDragMove = position => {
    
          if ( this.scramble !== null ) return;
          if ( this.state === STILL || ( this.state === ANIMATING && this.gettingDrag === false ) ) return;
    
          const planeIntersect = this.getIntersect( position.current, this.helper, false );
          if ( planeIntersect === false ) return;
    
          const point = this.helper.worldToLocal( planeIntersect.point.clone() );
    
          this.dragDelta = point.clone().sub( this.dragCurrent ).setZ( 0 );
          this.dragTotal.add( this.dragDelta );
          this.dragCurrent = point;
          this.addMomentumPoint( this.dragDelta );
    
          if ( this.state === PREPARING && this.dragTotal.length() > 0.05 ) {
    
            this.dragDirection = this.getMainAxis( this.dragTotal );
    
            if ( this.flipType === 'layer' ) {
    
              const direction = new THREE.Vector3();
              direction[ this.dragDirection ] = 1;
    
              const worldDirection = this.helper.localToWorld( direction ).sub( this.helper.position );
              const objectDirection = this.edges.worldToLocal( worldDirection ).round();
    
              this.flipAxis = objectDirection.cross( this.dragNormal ).negate();
    
              this.selectLayer( this.getLayer( false ) );
    
            } else {
    
              const axis = ( this.dragDirection != 'x' )
                ? ( ( this.dragDirection == 'y' && position.current.x > this.game.world.width / 2 ) ? 'z' : 'x' )
                : 'y';
    
              this.flipAxis = new THREE.Vector3();
              this.flipAxis[ axis ] = 1 * ( ( axis == 'x' ) ? - 1 : 1 );
    
            }
    
            this.flipAngle = 0;
            this.state = ROTATING;
    
          } else if ( this.state === ROTATING ) {
    
            const rotation = this.dragDelta[ this.dragDirection ];
    
            if ( this.flipType === 'layer' ) { 
    
              this.group.rotateOnAxis( this.flipAxis, rotation );
              this.flipAngle += rotation;
    
            } else {
    
              this.edges.rotateOnWorldAxis( this.flipAxis, rotation );
              this.game.cube.object.rotation.copy( this.edges.rotation );
              this.flipAngle += rotation;
    
            }
    
          }
    
        };
    
        this.draggable.onDragEnd = position => {
    
          if ( this.scramble !== null ) return;
          if ( this.state !== ROTATING ) {
    
            this.gettingDrag = false;
            this.state = STILL;
            return;
    
          }
    
          this.state = ANIMATING;
    
          const momentum = this.getMomentum()[ this.dragDirection ];
          const flip = ( Math.abs( momentum ) > 0.05 && Math.abs( this.flipAngle ) < Math.PI / 2 );
    
          const angle = flip
            ? this.roundAngle( this.flipAngle + Math.sign( this.flipAngle ) * ( Math.PI / 4 ) )
            : this.roundAngle( this.flipAngle );
    
          const delta = angle - this.flipAngle;
    
          if ( this.flipType === 'layer' ) {
    
            this.rotateLayer( delta, false, layer => {
    
              this.game.storage.saveGame();
              
              this.state = this.gettingDrag ? PREPARING : STILL;
              this.gettingDrag = false;
    
              this.checkIsSolved();
    
            } );
    
          } else {
    
            this.rotateCube( delta, () => {
    
              this.state = this.gettingDrag ? PREPARING : STILL;
              this.gettingDrag = false;
    
            } );
    
          }
    
        };
    
      }
    
      rotateLayer( rotation, scramble, callback ) {
    
        const config = scramble ? 0 : this.flipConfig;
    
        const easing = this.flipEasings[ config ];
        const duration = this.flipSpeeds[ config ];
        const bounce = ( config == 2 ) ? this.bounceCube() : ( () => {} );
    
        this.rotationTween = new Tween( {
          easing: easing,
          duration: duration,
          onUpdate: tween => {
    
            let deltaAngle = tween.delta * rotation;
            this.group.rotateOnAxis( this.flipAxis, deltaAngle );
            bounce( tween.value, deltaAngle, rotation );
    
          },
          onComplete: () => {
    
            if ( ! scramble ) this.onMove();
    
            const layer = this.flipLayer.slice( 0 );
    
            this.game.cube.object.rotation.setFromVector3( this.snapRotation( this.game.cube.object.rotation.toVector3() ) );
            this.group.rotation.setFromVector3( this.snapRotation( this.group.rotation.toVector3() ) );
            this.deselectLayer( this.flipLayer );
    
            callback( layer );
    
          },
        } );
    
      }
    
      bounceCube() {
    
        let fixDelta = true;
    
        return ( progress, delta, rotation ) => {
    
            if ( progress >= 1 ) {
    
              if ( fixDelta ) {
    
                delta = ( progress - 1 ) * rotation;
                fixDelta = false;
    
              }
    
              this.game.cube.object.rotateOnAxis( this.flipAxis, delta );
    
            }
    
        }
    
      }
    
      rotateCube( rotation, callback ) {
    
        const config = this.flipConfig;
        const easing = [ Easing.Power.Out( 4 ), Easing.Sine.Out(), Easing.Back.Out( 2 ) ][ config ];
        const duration = [ 100, 150, 350 ][ config ];
    
        this.rotationTween = new Tween( {
          easing: easing,
          duration: duration,
          onUpdate: tween => {
    
            this.edges.rotateOnWorldAxis( this.flipAxis, tween.delta * rotation );
            this.game.cube.object.rotation.copy( this.edges.rotation );
    
          },
          onComplete: () => {
    
            this.edges.rotation.setFromVector3( this.snapRotation( this.edges.rotation.toVector3() ) );
            this.game.cube.object.rotation.copy( this.edges.rotation );
            callback();
    
          },
        } );
    
      }
    
      selectLayer( layer ) {
    
        this.group.rotation.set( 0, 0, 0 );
        this.movePieces( layer, this.game.cube.object, this.group );
        this.flipLayer = layer;
    
      }
    
      deselectLayer( layer ) {
    
        this.movePieces( layer, this.group, this.game.cube.object );
        this.flipLayer = null;
    
      }
    
      movePieces( layer, from, to ) {
    
        from.updateMatrixWorld();
        to.updateMatrixWorld();
    
        layer.forEach( index => {
    
          const piece = this.game.cube.pieces[ index ];
    
          piece.applyMatrix( from.matrixWorld );
          from.remove( piece );
          piece.applyMatrix( new THREE.Matrix4().getInverse( to.matrixWorld ) );
          to.add( piece );
    
        } );
    
      }
    
      getLayer( position ) {
    
        const scalar = { 2: 6, 3: 3, 4: 4, 5: 3 }[ this.game.cube.size ];
        const layer = [];
    
        let axis;
    
        if ( position === false ) {
    
          const piece = this.dragIntersect.object.parent;
    
          axis = this.getMainAxis( this.flipAxis );
          position = piece.position.clone() .multiplyScalar( scalar ) .round();
    
        } else {
    
          axis = this.getMainAxis( position );
    
        }
    
        this.game.cube.pieces.forEach( piece => {
    
          const piecePosition = piece.position.clone().multiplyScalar( scalar ).round();
    
          if ( piecePosition[ axis ] == position[ axis ] ) layer.push( piece.name );
    
        } );
    
        return layer;
    
      }
    
      keyboardMove( type, move, callback ) {
    
        if ( this.state !== STILL ) return;
        if ( this.enabled !== true ) return;
    
        if ( type === 'LAYER' ) {
    
          const layer = this.getLayer( move.position );
    
          this.flipAxis = new THREE.Vector3();
          this.flipAxis[ move.axis ] = 1;
          this.state = ROTATING;
    
          this.selectLayer( layer );
          this.rotateLayer( move.angle, false, layer => {
    
            this.game.storage.saveGame();
            this.state = STILL;
            this.checkIsSolved();
    
          } );
    
        } else if ( type === 'CUBE' ) {
    
          this.flipAxis = new THREE.Vector3();
          this.flipAxis[ move.axis ] = 1;
          this.state = ROTATING;
    
          this.rotateCube( move.angle, () => {
    
            this.state = STILL;
    
          } );
    
        }
    
      }
    
      scrambleCube() {
    
        if ( this.scramble == null ) {
    
          this.scramble = this.game.scrambler;
          this.scramble.callback = ( typeof callback !== 'function' ) ? () => {} : callback;
    
        }
    
        const converted = this.scramble.converted;
        const move = converted[ 0 ];
        const layer = this.getLayer( move.position );
    
        this.flipAxis = new THREE.Vector3();
        this.flipAxis[ move.axis ] = 1;
    
        this.selectLayer( layer );
        this.rotateLayer( move.angle, true, () => {
    
          converted.shift();
    
          if ( converted.length > 0 ) {
    
            this.scrambleCube();
    
          } else {
    
            this.scramble = null;
            this.game.storage.saveGame();
    
          }
    
        } );
    
      }
    
      getIntersect( position, object, multiple ) {
    
        this.raycaster.setFromCamera(
          this.draggable.convertPosition( position.clone() ),
          this.game.world.camera
        );
    
        const intersect = ( multiple )
          ? this.raycaster.intersectObjects( object )
          : this.raycaster.intersectObject( object );
    
        return ( intersect.length > 0 ) ? intersect[ 0 ] : false;
    
      }
    
      getMainAxis( vector ) {
    
        return Object.keys( vector ).reduce(
          ( a, b ) => Math.abs( vector[ a ] ) > Math.abs( vector[ b ] ) ? a : b
        );
    
      }
    
      detach( child, parent ) {
    
        child.applyMatrix( parent.matrixWorld );
        parent.remove( child );
        this.game.world.scene.add( child );
    
      }
    
      attach( child, parent ) {
    
        child.applyMatrix( new THREE.Matrix4().getInverse( parent.matrixWorld ) );
        this.game.world.scene.remove( child );
        parent.add( child );
    
      }
    
      addMomentumPoint( delta ) {
    
        const time = Date.now();
    
        this.momentum = this.momentum.filter( moment => time - moment.time < 500 );
    
        if ( delta !== false ) this.momentum.push( { delta, time } );
    
      }
    
      getMomentum() {
    
        const points = this.momentum.length;
        const momentum = new THREE.Vector2();
    
        this.addMomentumPoint( false );
    
        this.momentum.forEach( ( point, index ) => {
    
          momentum.add( point.delta.multiplyScalar( index / points ) );
    
        } );
    
        return momentum;
    
      }
    
      roundAngle( angle ) {
    
        const round = Math.PI / 2;
        return Math.sign( angle ) * Math.round( Math.abs( angle) / round ) * round;
    
      }
    
      snapRotation( angle ) {
    
        return angle.set(
          this.roundAngle( angle.x ),
          this.roundAngle( angle.y ),
          this.roundAngle( angle.z )
        );
    
      }
    
      checkIsSolved() {
    
        const start = performance.now();
    
        let solved = true;
        const sides = { 'x-': [], 'x+': [], 'y-': [], 'y+': [], 'z-': [], 'z+': [] };
    
        this.game.cube.edges.forEach( edge => {
    
          const position = edge.parent
            .localToWorld( edge.position.clone() )
            .sub( this.game.cube.object.position );
    
          const mainAxis = this.getMainAxis( position );
          const mainSign = position.multiplyScalar( 2 ).round()[ mainAxis ] < 1 ? '-' : '+';
    
          sides[ mainAxis + mainSign ].push( edge.name );
    
        } );
    
        Object.keys( sides ).forEach( side => {
    
          if ( ! sides[ side ].every( value => value === sides[ side ][ 0 ] ) ) solved = false;
    
        } );
    
        if ( solved ) this.onSolved();
    
      }
    
    }
    
    class Scrambler {
    
      constructor( game ) {
    
        this.game = game;
    
        this.dificulty = 0;
    
        this.scrambleLength = {
          2: [ 7, 9, 11 ],
          3: [ 20, 25, 30 ],
          4: [ 30, 40, 50 ],
          5: [ 40, 60, 80 ],
        };
    
        this.moves = [];
        this.conveted = [];
        this.pring = '';
    
      }
    
      scramble( scramble ) {
    
        let count = 0;
        this.moves = ( typeof scramble !== 'undefined' ) ? scramble.split( ' ' ) : [];
    
        if ( this.moves.length < 1 ) {
    
          const scrambleLength = this.scrambleLength[ this.game.cube.size ][ this.dificulty ];
    
          const faces = this.game.cube.size < 4 ? 'UDLRFB' : 'UuDdLlRrFfBb';
          const modifiers = [ "", "'", "2" ];
          const total = ( typeof scramble === 'undefined' ) ? scrambleLength : scramble;
    
          while ( count < total ) {
    
            const move =
              faces[ Math.floor( Math.random() * faces.length ) ] +
              modifiers[ Math.floor( Math.random() * 3 ) ];
    
            if ( count > 0 && move.charAt( 0 ) == this.moves[ count - 1 ].charAt( 0 ) ) continue;
            if ( count > 1 && move.charAt( 0 ) == this.moves[ count - 2 ].charAt( 0 ) ) continue;
    
            this.moves.push( move );
            count ++;
    
          }
    
        }
    
        this.callback = () => {};
        this.convert();
        this.print = this.moves.join( ' ' );
    
        return this;
    
      }
    
      convert( moves ) {
    
        this.converted = [];
    
        this.moves.forEach( move => {
    
          const convertedMove = this.convertMove( move );
          const modifier = move.charAt( 1 );
    
          this.converted.push( convertedMove );
          if ( modifier == "2" ) this.converted.push( convertedMove );
    
        } );
    
      }
    
      convertMove( move ) {
    
        const face = move.charAt( 0 );
        const modifier = move.charAt( 1 );
    
        const axis = { D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[ face.toUpperCase() ];
        let row = { D: -1, U: 1, L: -1, R: 1, F: 1, B: -1 }[ face.toUpperCase() ];
    
        if ( this.game.cube.size > 3 && face !== face.toLowerCase() ) row = row * 2;
    
        const position = new THREE.Vector3();
        position[ { D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[ face.toUpperCase() ] ] = row;
    
        const angle = ( Math.PI / 2 ) * - row * ( ( modifier == "'" ) ? - 1 : 1 );
    
        return { position, axis, angle, name: move };
    
      }
    
    }
    
    class Transition {
    
      constructor( game ) {
    
        this.game = game;
    
        this.tweens = {};
        this.durations = {};
        this.data = {
          cubeY: -0.2,
          cameraZoom: 0.85,
        };
    
        this.activeTransitions = 0;
    
      }
    
      init() {
    
        this.game.controls.disable();
    
        this.game.cube.object.position.y = this.data.cubeY;
        this.game.cube.animator.position.y = 4;
        this.game.cube.animator.rotation.x = - Math.PI / 3;
        this.game.world.camera.zoom = this.data.cameraZoom;
        this.game.world.camera.updateProjectionMatrix();
    
        this.tweens.buttons = {};
        this.tweens.timer = [];
        this.tweens.title = [];
        this.tweens.best = [];
        this.tweens.complete = [];
        this.tweens.prefs = [];
        this.tweens.theme = [];
        this.tweens.stats = [];
    
      }
    
      buttons( show, hide ) {
    
        const buttonTween = ( button, show ) => {
    
          return new Tween( {
            target: button.style,
            duration: 300,
            easing: show ? Easing.Power.Out( 2 ) : Easing.Power.In( 3 ),
            from: { opacity: show ? 0 : 1 },
            to: { opacity: show ? 1 : 0 },
            onUpdate: tween => {
    
              const translate = show ? 1 - tween.value : tween.value;
              button.style.transform = `translate3d(0, ${translate * 1.5}em, 0)`;
    
            },
            onComplete: () => button.style.pointerEvents = show ? 'all' : 'none'
          } );
    
        };
    
        hide.forEach( button =>
          this.tweens.buttons[ button ] = buttonTween( this.game.dom.buttons[ button ], false )
        );
    
        setTimeout( () => show.forEach( button => {
    
          this.tweens.buttons[ button ] = buttonTween( this.game.dom.buttons[ button ], true );
    
        } ), hide ? 500 : 0 );
    
      }
    
      cube( show, theming = false ) {
    
        this.activeTransitions++;
    
        try { this.tweens.cube.stop(); } catch(e) {}
        const currentY = this.game.cube.animator.position.y;
        const currentRotation = this.game.cube.animator.rotation.x;
    
        this.tweens.cube = new Tween( {
          duration: show ? 3000 : 1250,
          easing: show ? Easing.Elastic.Out( 0.8, 0.6 ) : Easing.Back.In( 1 ),
          onUpdate: tween => {
    
            this.game.cube.animator.position.y = show
              ? ( theming ? 0.9 + ( 1 - tween.value ) * 3.5 : ( 1 - tween.value ) * 4 )
              : currentY + tween.value * 4;
    
            this.game.cube.animator.rotation.x = show
              ? ( 1 - tween.value ) * Math.PI / 3
              : currentRotation + tween.value * - Math.PI / 3;
    
          },
        } );
    
        if ( theming ) {
    
          if ( show ) {
    
            this.game.world.camera.zoom = 0.75;
            this.game.world.camera.updateProjectionMatrix();
    
          } else {
    
            setTimeout( () => {
    
              this.game.world.camera.zoom = this.data.cameraZoom;
              this.game.world.camera.updateProjectionMatrix();
    
            }, 1500 );
    
          }
    
        }
    
        this.durations.cube = show ? 1500 : 1500;
    
        setTimeout( () => this.activeTransitions--, this.durations.cube );
    
      }
    
      float() {
    
        try { this.tweens.float.stop(); } catch(e) {}
        this.tweens.float = new Tween( {
          duration: 1500,
          easing: Easing.Sine.InOut(),
          yoyo: true,
          onUpdate: tween => {
    
            this.game.cube.holder.position.y = (- 0.02 + tween.value * 0.04); 
            this.game.cube.holder.rotation.x = 0.005 - tween.value * 0.01;
            this.game.cube.holder.rotation.z = - this.game.cube.holder.rotation.x;
            this.game.cube.holder.rotation.y = this.game.cube.holder.rotation.x;
    
            this.game.controls.edges.position.y =
              this.game.cube.holder.position.y + this.game.cube.object.position.y;
    
          },
        } );
    
      }
    
      zoom( play, time ) {
    
        this.activeTransitions++;
    
        const zoom = ( play ) ? 1 : this.data.cameraZoom;
        const duration = ( time > 0 ) ? Math.max( time, 1500 ) : 1500;
        const rotations = ( time > 0 ) ? Math.round( duration / 1500 ) : 1;
        const easing = Easing.Power.InOut( ( time > 0 ) ? 2 : 3 );
    
        this.tweens.zoom = new Tween( {
          target: this.game.world.camera,
          duration: duration,
          easing: easing,
          to: { zoom: zoom },
          onUpdate: () => { this.game.world.camera.updateProjectionMatrix(); },
        } );
    
        this.tweens.rotate = new Tween( {
          target: this.game.cube.animator.rotation,
          duration: duration,
          easing: easing,
          to: { y: - Math.PI * 2 * rotations },
          onComplete: () => { this.game.cube.animator.rotation.y = 0; },
        } );
    
        this.durations.zoom = duration;
    
        setTimeout( () => this.activeTransitions--, this.durations.zoom );
    
      }
    
      elevate( complete ) {
    
        this.activeTransitions++;
    
        const cubeY = 
    
        this.tweens.elevate = new Tween( {
          target: this.game.cube.object.position,
          duration: complete ? 1500 : 0,
          easing: Easing.Power.InOut( 3 ),
          to: { y: complete ? -0.05 : this.data.cubeY }
        } );
    
        this.durations.elevate = 1500;
    
        setTimeout( () => this.activeTransitions--, this.durations.elevate );
    
      }
    
      complete( show, best ) {
    
        this.activeTransitions++;
    
        const text = best ? this.game.dom.texts.best : this.game.dom.texts.complete;
    
        if ( text.querySelector( 'span i' ) === null )
          text.querySelectorAll( 'span' ).forEach( span => this.splitLetters( span ) );
    
        const letters = text.querySelectorAll( '.icon, i' );
    
        this.flipLetters( best ? 'best' : 'complete', letters, show );
    
        text.style.opacity = 1;
    
        const duration = this.durations[ best ? 'best' : 'complete' ];
    
        if ( ! show ) setTimeout( () => this.game.dom.texts.timer.style.transform = '', duration );
    
        setTimeout( () => this.activeTransitions--, duration );
    
      } 
    
      stats( show ) {
    
        if ( show ) this.game.scores.calcStats();
    
        this.activeTransitions++;
    
        this.tweens.stats.forEach( tween => { tween.stop(); tween = null; } );
    
        let tweenId = -1;
    
        const stats = this.game.dom.stats.querySelectorAll( '.stats' );
        const easing = show ? Easing.Power.Out( 2 ) : Easing.Power.In( 3 );
    
        stats.forEach( ( stat, index ) => {
    
          const delay = index * ( show ? 80 : 60 );
    
          this.tweens.stats[ tweenId++ ] = new Tween( {
            delay: delay,
            duration: 400,
            easing: easing,
            onUpdate: tween => {
    
              const translate = show ? ( 1 - tween.value ) * 2 : tween.value;
              const opacity = show ? tween.value : ( 1 - tween.value );
    
              stat.style.transform = `translate3d(0, ${translate}em, 0)`;
              stat.style.opacity = opacity;
    
            }
          } );
    
        } );
    
        this.durations.stats = 0;
    
        setTimeout( () => this.activeTransitions--, this.durations.stats );
    
      }
    
      preferences( show ) {
    
        this.ranges( this.game.dom.prefs.querySelectorAll( '.range' ), 'prefs', show );
    
      }
    
      theming( show ) {
    
        this.ranges( this.game.dom.theme.querySelectorAll( '.range' ), 'prefs', show );
    
      }
    
      ranges( ranges, type, show ) {
    
        this.activeTransitions++;
    
        this.tweens[ type ].forEach( tween => { tween.stop(); tween = null; } );
    
        const easing = show ? Easing.Power.Out(2) : Easing.Power.In(3);
    
        let tweenId = -1;
        let listMax = 0;
    
        ranges.forEach( ( range, rangeIndex ) => {
        
          const label = range.querySelector( '.range__label' );
          const track = range.querySelector( '.range__track-line' );
          const handle = range.querySelector( '.range__handle' );
          const list = range.querySelectorAll( '.range__list div' );
    
          const delay = rangeIndex * ( show ? 120 : 100 );
    
          label.style.opacity = show ? 0 : 1;
          track.style.opacity = show ? 0 : 1;
          handle.style.opacity = show ? 0 : 1;
          handle.style.pointerEvents = show ? 'all' : 'none';
    
          this.tweens[ type ][ tweenId++ ] = new Tween( {
            delay: show ? delay : delay,
            duration: 400,
            easing: easing,
            onUpdate: tween => {
    
              const translate = show ? ( 1 - tween.value ) : tween.value;
              const opacity = show ? tween.value : ( 1 - tween.value );
    
              label.style.transform = `translate3d(0, ${translate}em, 0)`;
              label.style.opacity = opacity;
    
            }
          } );
    
          this.tweens[ type ][ tweenId++ ] = new Tween( {
            delay: show ? delay + 100 : delay,
            duration: 400,
            easing: easing,
            onUpdate: tween => {
    
              const translate = show ? ( 1 - tween.value ) : tween.value;
              const scale = show ? tween.value : ( 1 - tween.value );
              const opacity = scale;
    
              track.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, 1, 1)`;
              track.style.opacity = opacity;
    
            }
          } );
    
          this.tweens[ type ][ tweenId++ ] = new Tween( {
            delay: show ? delay + 100 : delay,
            duration: 400,
            easing: easing,
            onUpdate: tween => {
    
              const translate = show ? ( 1 - tween.value ) : tween.value;
              const opacity = 1 - translate;
              const scale = 0.5 + opacity * 0.5;
    
              handle.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, ${scale}, ${scale})`;
              handle.style.opacity = opacity;
    
            }
          } );
    
          list.forEach( ( listItem, labelIndex ) => {
    
            listItem.style.opacity = show ? 0 : 1;
    
            this.tweens[ type ][ tweenId++ ] = new Tween( {
              delay: show ? delay + 200 + labelIndex * 50 : delay,
              duration: 400,
              easing: easing,
              onUpdate: tween => {
    
                const translate = show ? ( 1 - tween.value ) : tween.value;
                const opacity = show ? tween.value : ( 1 - tween.value );
    
                listItem.style.transform = `translate3d(0, ${translate}em, 0)`;
                listItem.style.opacity = opacity;
    
              }
            } );
    
          } );
    
          listMax = list.length > listMax ? list.length - 1 : listMax;
    
          range.style.opacity = 1;
    
        } );
    
        this.durations[ type ] = show
          ? ( ( ranges.length - 1 ) * 100 ) + 200 + listMax * 50 + 400
          : ( ( ranges.length - 1 ) * 100 ) + 400;
    
        setTimeout( () => this.activeTransitions--, this.durations[ type ] ); 
    
      }
    
      title( show ) {
    
        this.activeTransitions++;
    
        const title = this.game.dom.texts.title;
    
        if ( title.querySelector( 'span i' ) === null )
          title.querySelectorAll( 'span' ).forEach( span => this.splitLetters( span ) );
    
        const letters = title.querySelectorAll( 'i' );
    
        this.flipLetters( 'title', letters, show );
    
        title.style.opacity = 1;
    
        const note = this.game.dom.texts.note;
    
        this.tweens.title[ letters.length ] = new Tween( {
          target: note.style,
          easing: Easing.Sine.InOut(),
          duration: show ? 800 : 400,
          yoyo: show ? true : null,
          from: { opacity: show ? 0 : ( parseFloat( getComputedStyle( note ).opacity ) ) },
          to: { opacity: show ? 1 : 0 },
        } );
    
        setTimeout( () => this.activeTransitions--, this.durations.title );
    
      }
    
      timer( show ) {
    
        this.activeTransitions++;
    
        const timer = this.game.dom.texts.timer;
    
        timer.style.opacity = 0;
        this.game.timer.convert();
        this.game.timer.setText();
    
        this.splitLetters( timer );
        const letters = timer.querySelectorAll( 'i' );
        this.flipLetters( 'timer', letters, show );
    
        timer.style.opacity = 1;
    
        setTimeout( () => this.activeTransitions--, this.durations.timer );
    
      }
    
      splitLetters( element ) {
    
        const text = element.innerHTML;
    
        element.innerHTML = '';
    
        text.split( '' ).forEach( letter => {
    
          const i = document.createElement( 'i' );
    
          i.innerHTML = letter;
    
          element.appendChild( i );
    
        } );
    
      }
    
      flipLetters( type, letters, show ) {
    
        try { this.tweens[ type ].forEach( tween => tween.stop() ); } catch(e) {}
        letters.forEach( ( letter, index ) => {
    
          letter.style.opacity = show ? 0 : 1;
    
          this.tweens[ type ][ index ] = new Tween( {
            easing: Easing.Sine.Out(),
            duration: show ? 800 : 400,
            delay: index * 50,
            onUpdate: tween => {
    
              const rotation = show ? ( 1 - tween.value ) * -80 : tween.value * 80;
    
              letter.style.transform = `rotate3d(0, 1, 0, ${rotation}deg)`;
              letter.style.opacity = show ? tween.value : ( 1 - tween.value );
    
            },
          } );
    
        } );
    
        this.durations[ type ] = ( letters.length - 1 ) * 50 + ( show ? 800 : 400 );
    
      }
    
    }
    
    class Timer extends Animation {
    
      constructor( game ) {
    
        super( false );
    
        this.game = game;
        this.reset();
        
      }
    
      start( continueGame ) {
    
        this.startTime = continueGame ? ( Date.now() - this.deltaTime ) : Date.now();
        this.deltaTime = 0;
        this.converted = this.convert();
    
        super.start();
    
      }
    
      reset() {
    
        this.startTime = 0;
        this.currentTime = 0;
        this.deltaTime = 0;
        this.converted = '0:00';
    
      }
    
      stop() {
    
        this.currentTime = Date.now();
        this.deltaTime = this.currentTime - this.startTime;
        this.convert();
    
        super.stop();
    
        return { time: this.converted, millis: this.deltaTime };
    
      }
    
      update() {
    
        const old = this.converted;
    
        this.currentTime = Date.now();
        this.deltaTime = this.currentTime - this.startTime;
        this.convert();
    
        if ( this.converted != old ) {
    
          localStorage.setItem( 'theCube_time', this.deltaTime );
          this.setText();
    
        }
    
      }
    
      convert() {
    
        const seconds = parseInt( ( this.deltaTime / 1000 ) % 60 );
        const minutes = parseInt( ( this.deltaTime / ( 1000 * 60 ) ) );
    
        this.converted = minutes + ':' + ( seconds < 10 ? '0' : '' ) + seconds;
    
      }
    
      setText() {
    
        this.game.dom.texts.timer.innerHTML = this.converted;
    
      }
    
    }
    
    const RangeHTML = [
    
      '<div class="range">',
        '<div class="range__label"></div>',
        '<div class="range__track">',
          '<div class="range__track-line"></div>',
          '<div class="range__handle"><div></div></div>',
        '</div>',
        '<div class="range__list"></div>',
      '</div>',
    
    ].join( '\n' );
    
    document.querySelectorAll( 'range' ).forEach( el => {
    
      const temp = document.createElement( 'div' );
      temp.innerHTML = RangeHTML;
    
      const range = temp.querySelector( '.range' );
      const rangeLabel = range.querySelector( '.range__label' );
      const rangeList = range.querySelector( '.range__list' );
    
      range.setAttribute( 'name', el.getAttribute( 'name' ) );
      rangeLabel.innerHTML = el.getAttribute( 'title' );
    
      if ( el.hasAttribute( 'color' ) ) {
    
        range.classList.add( 'range--type-color' );
        range.classList.add( 'range--color-' + el.getAttribute( 'name' ) );
    
      }
    
      if ( el.hasAttribute( 'list' ) ) {
    
        el.getAttribute( 'list' ).split( ',' ).forEach( listItemText => {
    
          const listItem = document.createElement( 'div' );
          listItem.innerHTML = listItemText;
          rangeList.appendChild( listItem );
    
        } );
    
      }
    
      el.parentNode.replaceChild( range, el );
    
    } );
    
    class Range {
    
      constructor( name, options ) {
    
        options = Object.assign( {
          range: [ 0, 1 ],
          value: 0,
          step: 0,
          onUpdate: () => {},
          onComplete: () => {},
        }, options || {} );
    
        this.element = document.querySelector( '.range[name="' + name + '"]' );
        this.track = this.element.querySelector( '.range__track' );
        this.handle = this.element.querySelector( '.range__handle' );
        this.list = [].slice.call( this.element.querySelectorAll( '.range__list div' ) );
    
        this.value = options.value;
        this.min = options.range[0];
        this.max = options.range[1];
        this.step = options.step;
    
        this.onUpdate = options.onUpdate;
        this.onComplete = options.onComplete;
    
        this.setValue( this.value );
    
        this.initDraggable();
    
      }
    
      setValue( value ) {
    
        this.value = this.round( this.limitValue( value ) );
        this.setHandlePosition();
    
      }
    
      initDraggable() {
    
        let current;
    
        this.draggable = new Draggable( this.handle, { calcDelta: true } );
    
        this.draggable.onDragStart = position => {
    
          current = this.positionFromValue( this.value );
          this.handle.style.left = current + 'px';
    
        };
    
        this.draggable.onDragMove = position => {
    
          current = this.limitPosition( current + position.delta.x );
          this.value = this.round( this.valueFromPosition( current ) );
          this.setHandlePosition();
          
          this.onUpdate( this.value );
    
        };
    
        this.draggable.onDragEnd = position => {
    
          this.onComplete( this.value );
    
        };
    
      }
    
      round( value ) {
    
        if ( this.step < 1 ) return value;
    
        return Math.round( ( value - this.min ) / this.step ) * this.step + this.min;
    
      }
    
      limitValue( value ) {
    
        const max = Math.max( this.max, this.min );
        const min = Math.min( this.max, this.min );
    
        return Math.min( Math.max( value, min ), max );
    
      }
    
      limitPosition( position ) {
    
        return Math.min( Math.max( position, 0 ), this.track.offsetWidth );
    
      }
    
      percentsFromValue( value ) {
    
        return ( value - this.min ) / ( this.max - this.min );
    
      }
    
      valueFromPosition( position ) {
    
        return this.min + ( this.max - this.min ) * ( position / this.track.offsetWidth );
    
      }
    
      positionFromValue( value ) {
    
        return this.percentsFromValue( value ) * this.track.offsetWidth;
    
      }
    
      setHandlePosition() {
    
        this.handle.style.left = this.percentsFromValue( this.value ) * 100 + '%';
    
      }
    
    }
    
    class Preferences {
    
      constructor( game ) {
    
        this.game = game;
    
      }
    
      init() {
    
        this.ranges = {
    
          size: new Range( 'size', {
            value: this.game.cube.size,
            range: [ 2, 5 ],
            step: 1,
            onUpdate: value => {
    
              this.game.cube.size = value;
    
              this.game.preferences.ranges.scramble.list.forEach( ( item, i ) => {
    
                item.innerHTML = this.game.scrambler.scrambleLength[ this.game.cube.size ][ i ];
    
              } );
    
            },
            onComplete: () => this.game.storage.savePreferences(),
          } ),
    
          flip: new Range( 'flip', {
            value: this.game.controls.flipConfig,
            range: [ 0, 2 ],
            step: 1,
            onUpdate: value => {
    
              this.game.controls.flipConfig = value;
    
            },
            onComplete: () => this.game.storage.savePreferences(),
          } ),
    
          scramble: new Range( 'scramble', {
            value: this.game.scrambler.dificulty,
            range: [ 0, 2 ],
            step: 1,
            onUpdate: value => {
    
              this.game.scrambler.dificulty = value;
    
            },
            onComplete: () => this.game.storage.savePreferences()
          } ),
    
          fov: new Range( 'fov', {
            value: this.game.world.fov,
            range: [ 2, 45 ],
            onUpdate: value => {
    
              this.game.world.fov = value;
              this.game.world.resize();
    
            },
            onComplete: () => this.game.storage.savePreferences()
          } ),
    
          theme: new Range( 'theme', {
            value: { cube: 0, erno: 1, dust: 2, camo: 3, rain: 4 }[ this.game.themes.theme ],
            range: [ 0, 4 ],
            step: 1,
            onUpdate: value => {
    
              const theme = [ 'cube', 'erno', 'dust', 'camo', 'rain' ][ value ];
              this.game.themes.setTheme( theme );
    
            },
            onComplete: () => this.game.storage.savePreferences()
          } ),
    
          hue: new Range( 'hue', {
            value: 0,
            range: [ 0, 360 ],
            onUpdate: value => this.game.themeEditor.updateHSL(),
            onComplete: () => this.game.storage.savePreferences(),
          } ),
    
          saturation: new Range( 'saturation', {
            value: 100,
            range: [ 0, 100 ],
            onUpdate: value => this.game.themeEditor.updateHSL(),
            onComplete: () => this.game.storage.savePreferences(),
          } ),
    
          lightness: new Range( 'lightness', {
            value: 50,
            range: [ 0, 100 ],
            onUpdate: value => this.game.themeEditor.updateHSL(),
            onComplete: () => this.game.storage.savePreferences(),
          } ),
    
        };
    
        this.ranges.scramble.list.forEach( ( item, i ) => {
    
          item.innerHTML = this.game.scrambler.scrambleLength[ this.game.cube.size ][ i ];
    
        } );
        
      }
    
    }
    
    class Confetti {
    
      constructor( game ) {
    
        this.game = game;
        this.started = 0;
    
        this.options = {
          speed: { min: 0.0011, max: 0.0022 },
          revolution: { min: 0.01, max: 0.05 },
          size: { min: 0.1, max: 0.15 },
          colors: [ 0x41aac8, 0x82ca38, 0xffef48, 0xef3923, 0xff8c0a ],
        };
    
        this.geometry = new THREE.PlaneGeometry( 1, 1 );
        this.material = new THREE.MeshLambertMaterial( { side: THREE.DoubleSide } );
    
        this.holders = [
          new ConfettiStage( this.game, this, 1, 20 ),
          new ConfettiStage( this.game, this, -1, 30 ),
        ];
    
      }
    
      start() {
    
        if ( this.started > 0 ) return;
    
        this.holders.forEach( holder => {
    
          this.game.world.scene.add( holder.holder );
          holder.start();
          this.started ++;
    
        } );
    
      }
    
      stop() {
    
        if ( this.started == 0 ) return;
    
        this.holders.forEach( holder => {
    
          holder.stop( () => {
    
            this.game.world.scene.remove( holder.holder );
            this.started --;
    
          } );
    
        } );
    
      }
    
      updateColors( colors ) {
    
        this.holders.forEach( holder => {
    
          holder.options.colors.forEach( ( color, index ) => {
    
            holder.options.colors[ index ] = colors[ [ 'D', 'F', 'R', 'B', 'L' ][ index ] ];
    
          } );
    
        } );
    
      }
    
    }
    
    class ConfettiStage extends Animation {
    
      constructor( game, parent, distance, count ) {
    
        super( false );
    
        this.game = game;
        this.parent = parent;
    
        this.distanceFromCube = distance;
    
        this.count = count;
        this.particles = [];
    
        this.holder = new THREE.Object3D();
        this.holder.rotation.copy( this.game.world.camera.rotation );
    
        this.object = new THREE.Object3D();
        this.holder.add( this.object );
    
        this.resizeViewport = this.resizeViewport.bind( this );
        this.game.world.onResize.push( this.resizeViewport );
        this.resizeViewport();    
    
        this.geometry = this.parent.geometry;
        this.material = this.parent.material;
    
        this.options = this.parent.options;
    
        let i = this.count;
        while ( i-- ) this.particles.push( new Particle( this ) );
    
      }
    
      start() {
    
        this.time = performance.now();
        this.playing = true;
    
        let i = this.count;
        while ( i-- ) this.particles[ i ].reset();
    
        super.start();
    
      }
    
      stop( callback ) {
    
        this.playing = false;
        this.completed = 0;
        this.callback = callback;
    
      }
    
      reset() {
    
        super.stop();
    
        this.callback();
    
      }
    
      update() {
    
        const now = performance.now();
        const delta = now - this.time;
        this.time = now;
    
        let i = this.count;
    
        while ( i-- )
          if ( ! this.particles[ i ].completed ) this.particles[ i ].update( delta );
    
        if ( ! this.playing && this.completed == this.count ) this.reset();
    
      }
    
      resizeViewport() {
    
        const fovRad = this.game.world.camera.fov * THREE.Math.DEG2RAD;
    
        this.height = 2 * Math.tan( fovRad / 2 ) * ( this.game.world.camera.position.length() - this.distanceFromCube );
        this.width = this.height * this.game.world.camera.aspect;
    
        const scale = 1 / this.game.transition.data.cameraZoom;
    
        this.width *= scale;
        this.height *= scale;
    
        this.object.position.z = this.distanceFromCube;
        this.object.position.y = this.height / 2;
    
      }
      
    }
    
    class Particle {
    
      constructor( confetti ) {
    
        this.confetti = confetti;
        this.options = this.confetti.options;
    
        this.velocity = new THREE.Vector3();
        this.force = new THREE.Vector3();
    
        this.mesh = new THREE.Mesh( this.confetti.geometry, this.confetti.material.clone() );
        this.confetti.object.add( this.mesh );
    
        this.size = THREE.Math.randFloat( this.options.size.min, this.options.size.max );
        this.mesh.scale.set( this.size, this.size, this.size );
    
        return this;
    
      }
    
      reset( randomHeight = true ) {
    
        this.completed = false;
    
        this.color = new THREE.Color( this.options.colors[ Math.floor( Math.random() * this.options.colors.length ) ] );
        this.mesh.material.color.set( this.color );
    
        this.speed = THREE.Math.randFloat( this.options.speed.min, this.options.speed.max ) * - 1;
        this.mesh.position.x = THREE.Math.randFloat( - this.confetti.width / 2, this.confetti.width / 2 );
        this.mesh.position.y = ( randomHeight )
          ? THREE.Math.randFloat( this.size, this.confetti.height + this.size )
          : this.size;
    
        this.revolutionSpeed = THREE.Math.randFloat( this.options.revolution.min, this.options.revolution.max );
        this.revolutionAxis = [ 'x', 'y', 'z' ][ Math.floor( Math.random() * 3 ) ];
        this.mesh.rotation.set( Math.random() * Math.PI / 3, Math.random() * Math.PI / 3, Math.random() * Math.PI / 3 );
    
      }
    
      stop() {
    
        this.completed = true;
        this.confetti.completed ++;
    
      }
    
      update( delta ) {
    
        this.mesh.position.y += this.speed * delta;
        this.mesh.rotation[ this.revolutionAxis ] += this.revolutionSpeed;
    
        if ( this.mesh.position.y < - this.confetti.height - this.size )
          ( this.confetti.playing ) ? this.reset( false ) : this.stop();
    
      }
    
    }
    
    class Scores {
    
      constructor( game ) {
    
        this.game = game;
    
        this.data = {
          2: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
          },
          3: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
          },
          4: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
          },
          5: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
          }
        };
    
      }
    
      addScore( time ) {
    
        const data = this.data[ this.game.cube.sizeGenerated ];
    
        data.scores.push( time );
        data.solves++;
    
        if ( data.scores.lenght > 100 ) data.scores.shift();
    
        let bestTime = false;    
    
        if ( time < data.best || data.best === 0 ) {
    
          data.best = time;
          bestTime = true;
    
        }
    
        if ( time > data.worst ) data.worst = time;
    
        this.game.storage.saveScores();
    
        return bestTime;
    
      }
    
      calcStats() {
    
        const s = this.game.cube.sizeGenerated;
        const data = this.data[ s ];
    
        this.setStat( 'cube-size', `${s}<i>x</i>${s}<i>x</i>${s}` );
        this.setStat( 'total-solves', data.solves );
        this.setStat( 'best-time', this.convertTime( data.best ) );
        this.setStat( 'worst-time', this.convertTime( data.worst ) );
        this.setStat( 'average-5', this.getAverage( 5 ) );
        this.setStat( 'average-12', this.getAverage( 12 ) );
        this.setStat( 'average-25', this.getAverage( 25 ) );
    
      }
    
      setStat( name, value ) {
    
        if ( value === 0 ) value = '-';
    
        this.game.dom.stats.querySelector( `.stats[name="${name}"] b` ).innerHTML = value;
    
      }
    
      getAverage( count ) {
    
        const data = this.data[ this.game.cube.sizeGenerated ];
    
        if ( data.scores.length < count ) return 0;
    
        return this.convertTime( data.scores.slice( -count ).reduce( ( a, b ) => a + b, 0 ) / count );
    
      }
    
      convertTime( time ) {
    
        if ( time <= 0 ) return 0;
    
        const seconds = parseInt( ( time / 1000 ) % 60 );
        const minutes = parseInt( ( time / ( 1000 * 60 ) ) );
    
        return minutes + ':' + ( seconds < 10 ? '0' : '' ) + seconds;
    
      }
    
    }
    
    class Storage {
    
      constructor( game ) {
    
        this.game = game;
    
        const userVersion = localStorage.getItem( 'theCube_version' );
    
        if ( ! userVersion || userVersion !== window.gameVersion ) {
    
          this.clearGame();
          this.clearPreferences();
          this.migrateScores();
          localStorage.setItem( 'theCube_version', window.gameVersion );
    
        }
    
      }
    
      init() {
    
        this.loadPreferences();
        this.loadScores();
    
      }
    
      loadGame() {
    
        try {
    
          const gameInProgress = localStorage.getItem( 'theCube_playing' ) === 'true';
    
          if ( ! gameInProgress ) throw new Error();
    
          const gameCubeData = JSON.parse( localStorage.getItem( 'theCube_savedState' ) );
          const gameTime = parseInt( localStorage.getItem( 'theCube_time' ) );
    
          if ( ! gameCubeData || gameTime === null ) throw new Error();
          if ( gameCubeData.size !== this.game.cube.sizeGenerated ) throw new Error();
    
          this.game.cube.loadFromData( gameCubeData );
    
          this.game.timer.deltaTime = gameTime;
    
          this.game.saved = true;
    
        } catch( e ) {
    
          this.game.saved = false;
    
        }
    
      }
    
      saveGame() {
    
        const gameInProgress = true;
        const gameCubeData = { names: [], positions: [], rotations: [] };
        const gameTime = this.game.timer.deltaTime;
    
        gameCubeData.size = this.game.cube.sizeGenerated;
    
        this.game.cube.pieces.forEach( piece => {
    
          gameCubeData.names.push( piece.name );
          gameCubeData.positions.push( piece.position );
          gameCubeData.rotations.push( piece.rotation.toVector3() );
    
        } );
    
        localStorage.setItem( 'theCube_playing', gameInProgress );
        localStorage.setItem( 'theCube_savedState', JSON.stringify( gameCubeData ) );
        localStorage.setItem( 'theCube_time', gameTime );
    
      }
    
      clearGame() {
    
        localStorage.removeItem( 'theCube_playing' );
        localStorage.removeItem( 'theCube_savedState' );
        localStorage.removeItem( 'theCube_time' );
    
      }
    
      loadScores() {
    
        try {
    
          const scoresData = JSON.parse( localStorage.getItem( 'theCube_scores' ) );
    
          if ( ! scoresData ) throw new Error();
    
          this.game.scores.data = scoresData;
    
        } catch( e ) {}
    
      }
    
      saveScores() {
    
        const scoresData = this.game.scores.data;
    
        localStorage.setItem( 'theCube_scores', JSON.stringify( scoresData ) );
    
      }
    
      clearScores() {
    
        localStorage.removeItem( 'theCube_scores' );
    
      }
    
      migrateScores() {
    
        try {
    
          const scoresData = JSON.parse( localStorage.getItem( 'theCube_scoresData' ) );
          const scoresBest = parseInt( localStorage.getItem( 'theCube_scoresBest' ) );
          const scoresWorst = parseInt( localStorage.getItem( 'theCube_scoresWorst' ) );
          const scoresSolves = parseInt( localStorage.getItem( 'theCube_scoresSolves' ) );
    
          if ( ! scoresData || ! scoresBest || ! scoresSolves || ! scoresWorst ) return false;
    
          this.game.scores.data[ 3 ].scores = scoresData;
          this.game.scores.data[ 3 ].best = scoresBest;
          this.game.scores.data[ 3 ].solves = scoresSolves;
          this.game.scores.data[ 3 ].worst = scoresWorst;
    
          localStorage.removeItem( 'theCube_scoresData' );
          localStorage.removeItem( 'theCube_scoresBest' );
          localStorage.removeItem( 'theCube_scoresWorst' );
          localStorage.removeItem( 'theCube_scoresSolves' );
    
        } catch( e ) {}
    
      }
    
      loadPreferences() {
    
        try {
    
          const preferences = JSON.parse( localStorage.getItem( 'theCube_preferences' ) );
    
          if ( ! preferences ) throw new Error();
    
          this.game.cube.size = parseInt( preferences.cubeSize );
          this.game.controls.flipConfig = parseInt( preferences.flipConfig );
          this.game.scrambler.dificulty = parseInt( preferences.dificulty );
    
          this.game.world.fov = parseFloat( preferences.fov );
          this.game.world.resize();
    
          this.game.themes.colors = preferences.colors;
          this.game.themes.setTheme( preferences.theme );
    
          return true;
    
        } catch (e) {
    
          this.game.cube.size = 3;
          this.game.controls.flipConfig = 0;
          this.game.scrambler.dificulty = 1;
    
          this.game.world.fov = 10;
          this.game.world.resize();
    
          this.game.themes.setTheme( 'cube' );
    
          this.savePreferences();
    
          return false;
    
        }
    
      }
    
      savePreferences() {
    
        const preferences = {
          cubeSize: this.game.cube.size,
          flipConfig: this.game.controls.flipConfig,
          dificulty: this.game.scrambler.dificulty,
          fov: this.game.world.fov,
          theme: this.game.themes.theme,
          colors: this.game.themes.colors,
        };
    
        localStorage.setItem( 'theCube_preferences', JSON.stringify( preferences ) );
    
      }
    
      clearPreferences() {
    
        localStorage.removeItem( 'theCube_preferences' );
    
      }
    
    }
    
    class Themes {
    
      constructor( game ) {
    
        this.game = game;
        this.theme = null;
    
        this.defaults = {
          cube: {
            U: 0xfff7ff, // white
            D: 0xffef48, // yellow
            F: 0xef3923, // red
            R: 0x41aac8, // blue
            B: 0xff8c0a, // orange
            L: 0x82ca38, // green
            P: 0x08101a, // piece
            G: 0xd1d5db, // background
          },
          erno: {
            U: 0xffffff,
            D: 0xffd500,
            F: 0xc41e3a,
            R: 0x0051ba,
            B: 0xff5800,
            L: 0x009e60,
            P: 0x08101a,
            G: 0x8abdff,
          },
          dust: {
            U: 0xfff6eb,
            D: 0xe7c48d,
            F: 0x8f253e,
            R: 0x607e69,
            B: 0xbe6f62,
            L: 0x849f5d,
            P: 0x08101a,
            G: 0xE7C48D,
          },
          camo: {
            U: 0xfff6eb,
            D: 0xbfb672,
            F: 0x37241c,
            R: 0x718456,
            B: 0x805831,
            L: 0x37431d,
            P: 0x08101a,
            G: 0xBFB672,
          },
          rain: {
            U: 0xfafaff,
            D: 0xedb92d,
            F: 0xce2135,
            R: 0x449a89,
            B: 0xec582f,
            L: 0xa3a947,
            P: 0x08101a,
            G: 0x87b9ac,
          },
        };
    
        this.colors = JSON.parse( JSON.stringify( this.defaults ) );
    
      }
    
      getColors() {
    
        return this.colors[ this.theme ];
    
      }
    
      setTheme( theme = false, force = false ) {
    
        if ( theme === this.theme && force === false ) return;
        if ( theme !== false ) this.theme = theme;
    
        const colors = this.getColors();
    
        this.game.dom.prefs.querySelectorAll( '.range__handle div' ).forEach( range => {
    
          range.style.background = '#' + colors.R.toString(16).padStart(6, '0');
    
        } );
    
        this.game.cube.updateColors( colors );
    
        this.game.confetti.updateColors( colors );
    
        this.game.dom.back.style.background = '#' + colors.G.toString(16).padStart(6, '0');
    
      }
    
    }
    
    class ThemeEditor {
    
      constructor( game ) {
    
        this.game = game;
    
        this.editColor = 'R';
    
        this.getPieceColor = this.getPieceColor.bind( this );
    
      }
    
      colorFromHSL( h, s, l ) {
    
        h = Math.round( h );
        s = Math.round( s );
        l = Math.round( l );
    
        return new THREE.Color( `hsl(${h}, ${s}%, ${l}%)` );
    
      }
    
      setHSL( color = null, animate = false ) {
    
        this.editColor = ( color === null) ? 'R' : color;
    
        const hsl = new THREE.Color( this.game.themes.getColors()[ this.editColor ] );
    
        const { h, s, l } = hsl.getHSL( hsl );
        const { hue, saturation, lightness } = this.game.preferences.ranges;
    
        if ( animate ) {
    
          const ho = hue.value / 360;
          const so = saturation.value / 100;
          const lo = lightness.value / 100;
    
          const colorOld = this.colorFromHSL( hue.value, saturation.value, lightness.value );
    
          if ( this.tweenHSL ) this.tweenHSL.stop();
    
          this.tweenHSL = new Tween( {
            duration: 200,
            easing: Easing.Sine.Out(),
            onUpdate: tween => {
    
              hue.setValue( ( ho + ( h - ho ) * tween.value ) * 360 );
              saturation.setValue( ( so + ( s - so ) * tween.value ) * 100 );
              lightness.setValue( ( lo + ( l - lo ) * tween.value ) * 100 );
    
              const colorTween = colorOld.clone().lerp( hsl, tween.value );
    
              const colorTweenStyle = colorTween.getStyle();
              const colorTweenHex = colorTween.getHSL( colorTween );
    
              hue.handle.style.color = colorTweenStyle;
              saturation.handle.style.color = colorTweenStyle;
              lightness.handle.style.color = colorTweenStyle;
    
              saturation.track.style.color =
                this.colorFromHSL( colorTweenHex.h * 360, 100, 50 ).getStyle();
              lightness.track.style.color =
                this.colorFromHSL( colorTweenHex.h * 360, colorTweenHex.s * 100, 50 ).getStyle();
    
              this.game.dom.theme.style.display = 'none';
              this.game.dom.theme.offsetHeight;
              this.game.dom.theme.style.display = '';
    
            },
            onComplete: () => {
    
              this.updateHSL();
              this.game.storage.savePreferences();
    
            },
          } );
    
        } else {
    
          hue.setValue( h * 360 );
          saturation.setValue( s * 100 );
          lightness.setValue( l * 100 );
    
          this.updateHSL();
          this.game.storage.savePreferences();
    
        }
    
      }
    
      updateHSL() {
    
        const { hue, saturation, lightness } = this.game.preferences.ranges;
    
        const h = hue.value;
        const s = saturation.value;
        const l = lightness.value;
    
        const color = this.colorFromHSL( h, s, l ).getStyle();
    
        hue.handle.style.color = color;
        saturation.handle.style.color = color;
        lightness.handle.style.color = color;
    
        saturation.track.style.color = this.colorFromHSL( h, 100, 50 ).getStyle();
        lightness.track.style.color = this.colorFromHSL( h, s, 50 ).getStyle();
    
        this.game.dom.theme.style.display = 'none';
        this.game.dom.theme.offsetHeight;
        this.game.dom.theme.style.display = '';
    
        const theme = this.game.themes.theme;
    
        this.game.themes.colors[ theme ][ this.editColor ] = this.colorFromHSL( h, s, l ).getHex();
        this.game.themes.setTheme();
    
      }
    
      colorPicker( enable ) {
    
        if ( enable ) {
    
          this.game.dom.game.addEventListener( 'click', this.getPieceColor, false );
    
        } else {
    
          this.game.dom.game.removeEventListener( 'click', this.getPieceColor, false );
    
        }
    
      }
    
      getPieceColor( event ) {
    
        const clickEvent = event.touches
          ? ( event.touches[ 0 ] || event.changedTouches[ 0 ] )
          : event;
    
        const clickPosition = new THREE.Vector2( clickEvent.pageX, clickEvent.pageY );
    
        let edgeIntersect = this.game.controls.getIntersect( clickPosition, this.game.cube.edges, true );
        let pieceIntersect = this.game.controls.getIntersect( clickPosition, this.game.cube.cubes, true );
    
        if ( edgeIntersect !== false ) {
    
          const edge = edgeIntersect.object;
    
          const position = edge.parent
            .localToWorld( edge.position.clone() )
            .sub( this.game.cube.object.position )
            .sub( this.game.cube.animator.position );
    
          const mainAxis = this.game.controls.getMainAxis( position );
          if ( position.multiplyScalar( 2 ).round()[ mainAxis ] < 1 ) edgeIntersect = false;
    
        }
    
        const name = edgeIntersect ? edgeIntersect.object.name : pieceIntersect ? 'P' : 'G';
    
        this.setHSL( name, true );
    
      }
    
      resetTheme() {
    
        this.game.themes.colors[ this.game.themes.theme ] =
          JSON.parse( JSON.stringify( this.game.themes.defaults[ this.game.themes.theme ] ) );
    
        this.game.themes.setTheme();
    
        this.setHSL( this.editColor, true );
    
      }
    
    }
    
    const States = {
      3: {
        checkerboard: {
          names: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 ],
          positions: [
            { "x": 1/3, "y": -1/3, "z": 1/3 },
            { "x": -1/3, "y": 1/3, "z": 0 },
            { "x": 1/3, "y": -1/3, "z": -1/3 },
            { "x": -1/3, "y": 0, "z": -1/3 },
            { "x": 1/3, "y": 0, "z": 0 },
            { "x": -1/3, "y": 0, "z": 1/3 },
            { "x": 1/3, "y": 1/3, "z": 1/3 },
            { "x": -1/3, "y": -1/3, "z": 0 },
            { "x": 1/3, "y": 1/3, "z": -1/3 },
            { "x": 0, "y": 1/3, "z": -1/3 },
            { "x": 0, "y": -1/3, "z": 0 },
            { "x": 0, "y": 1/3, "z": 1/3 },
            { "x": 0, "y": 0, "z": 1/3 },
            { "x": 0, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": -1/3 },
            { "x": 0, "y": -1/3, "z": -1/3 },
            { "x": 0, "y": 1/3, "z": 0 },
            { "x": 0, "y": -1/3, "z": 1/3 },
            { "x": -1/3, "y": -1/3, "z": 1/3 },
            { "x": 1/3, "y": 1/3, "z": 0 },
            { "x": -1/3, "y": -1/3, "z": -1/3 },
            { "x": 1/3, "y": 0, "z": -1/3 },
            { "x": -1/3, "y": 0, "z": 0 },
            { "x": 1/3, "y": 0, "z": 1/3 },
            { "x": -1/3, "y": 1/3, "z": 1/3 },
            { "x": 1/3, "y": -1/3, "z": 0 },
            { "x": -1/3, "y": 1/3, "z": -1/3 }
          ],
          rotations: [
            { "x": -Math.PI, "y": 0, "z": Math.PI, },
            { "x": Math.PI, "y": 0, "z": 0 },
            { "x": -Math.PI, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": -Math.PI, "y": 0, "z": Math.PI },
            { "x": Math.PI, "y": 0, "z": 0 },
            { "x": -Math.PI, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": -Math.PI, "y": 0, "z": 0 },
            { "x": Math.PI, "y": 0, "z": Math.PI },
            { "x": Math.PI, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": Math.PI, "y": 0, "z": Math.PI },
            { "x": -Math.PI, "y": 0, "z": 0 },
            { "x": Math.PI, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": 0, "y": 0, "z": Math.PI },
            { "x": 0, "y": 0, "z": 0 },
            { "x": Math.PI, "y": 0, "z": Math.PI },
            { "x": -Math.PI, "y": 0, "z": 0 },
            { "x": Math.PI, "y": 0, "z": Math.PI }
          ],
          size: 3,
        },
      }
    };
    
    class IconsConverter {
    
      constructor( options ) {
    
        options = Object.assign( {
          tagName: 'icon',
          className: 'icon',
          styles: false,
          icons: {},
          observe: false,
          convert: false,
        }, options || {} );
    
        this.tagName = options.tagName;
        this.className = options.className;
        this.icons = options.icons;
    
        this.svgTag = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
        this.svgTag.setAttribute( 'class', this.className );
    
        if ( options.styles ) this.addStyles();
        if ( options.convert ) this.convertAllIcons();
    
        if ( options.observe ) {
    
          const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
          this.observer = new MutationObserver( mutations => { this.convertAllIcons(); } );
          this.observer.observe( document.documentElement, { childList: true, subtree: true } );
    
        }
    
        return this;
    
      }
    
      convertAllIcons() {
    
        document.querySelectorAll( this.tagName ).forEach( icon => { this.convertIcon( icon ); } );
    
      }
    
      convertIcon( icon ) {
    
        const svgData = this.icons[ icon.attributes[0].localName ];
    
        if ( typeof svgData === 'undefined' ) return;
    
        const svg = this.svgTag.cloneNode( true );
        const viewBox = svgData.viewbox.split( ' ' );
    
        svg.setAttributeNS( null, 'viewBox', svgData.viewbox );
        svg.style.width = viewBox[2] / viewBox[3] + 'em';
        svg.style.height = '1em';
        svg.innerHTML = svgData.content;
    
        icon.parentNode.replaceChild( svg, icon );
    
      }
    
      addStyles() {
    
        const style = document.createElement( 'style' );
        style.innerHTML = `.${this.className} { display: inline-block; font-size: inherit; overflow: visible; vertical-align: -0.125em; preserveAspectRatio: none; }`;
        document.head.appendChild( style );
    
      }
    
    }
    
    const Icons = new IconsConverter( {
    
      icons: {
        settings: {
          viewbox: '0 0 512 512',
          content: '<path fill="currentColor" d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z" />',
        },
        back: {
          viewbox: '0 0 512 512',
          content: '<path transform="translate(512, 0) scale(-1,1)" fill="currentColor" d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z" />',
        },
        trophy: {
          viewbox: '0 0 576 512',
          content: '<path fill="currentColor" d="M552 64H448V24c0-13.3-10.7-24-24-24H152c-13.3 0-24 10.7-24 24v40H24C10.7 64 0 74.7 0 88v56c0 66.5 77.9 131.7 171.9 142.4C203.3 338.5 240 360 240 360v72h-48c-35.3 0-64 20.7-64 56v12c0 6.6 5.4 12 12 12h296c6.6 0 12-5.4 12-12v-12c0-35.3-28.7-56-64-56h-48v-72s36.7-21.5 68.1-73.6C498.4 275.6 576 210.3 576 144V88c0-13.3-10.7-24-24-24zM64 144v-16h64.2c1 32.6 5.8 61.2 12.8 86.2-47.5-16.4-77-49.9-77-70.2zm448 0c0 20.2-29.4 53.8-77 70.2 7-25 11.8-53.6 12.8-86.2H512v16zm-127.3 4.7l-39.6 38.6 9.4 54.6c1.7 9.8-8.7 17.2-17.4 12.6l-49-25.8-49 25.8c-8.8 4.6-19.1-2.9-17.4-12.6l9.4-54.6-39.6-38.6c-7.1-6.9-3.2-19 6.7-20.5l54.8-8 24.5-49.6c4.4-8.9 17.1-8.9 21.5 0l24.5 49.6 54.8 8c9.6 1.5 13.5 13.6 6.4 20.5z" />',
        },
        cancel: {
          viewbox: '0 0 352 512',
          content: '<path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />',
        },
        theme: {
          viewbox: '0 0 512 512',
          content: '<path fill="currentColor" d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"/>',
        },
        reset: {
          viewbox: '0 0 512 512',
          content: '<path fill="currentColor" d="M370.72 133.28C339.458 104.008 298.888 87.962 255.848 88c-77.458.068-144.328 53.178-162.791 126.85-1.344 5.363-6.122 9.15-11.651 9.15H24.103c-7.498 0-13.194-6.807-11.807-14.176C33.933 94.924 134.813 8 256 8c66.448 0 126.791 26.136 171.315 68.685L463.03 40.97C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.749zM32 296h134.059c21.382 0 32.09 25.851 16.971 40.971l-41.75 41.75c31.262 29.273 71.835 45.319 114.876 45.28 77.418-.07 144.315-53.144 162.787-126.849 1.344-5.363 6.122-9.15 11.651-9.15h57.304c7.498 0 13.194 6.807 11.807 14.176C478.067 417.076 377.187 504 256 504c-66.448 0-126.791-26.136-171.315-68.685L48.97 471.03C33.851 486.149 8 475.441 8 454.059V320c0-13.255 10.745-24 24-24z" />',
        },
        trash: {
          viewbox: '0 0 448 512',
          content: '<path fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" />',
        },
      },
    
      convert: true,
    
    } );
    
    const STATE = {
      Menu: 0,
      Playing: 1,
      Complete: 2,
      Stats: 3,
      Prefs: 4,
      Theme: 5,
    };
    
    const BUTTONS = {
      Menu: [ 'stats', 'prefs' ],
      Playing: [ 'back' ],
      Complete: [],
      Stats: [],
      Prefs: [ 'back', 'theme' ],
      Theme: [ 'back', 'reset' ],
      None: [],
    };
    
    const SHOW = true;
    const HIDE = false;
    
    class Game {
    
      constructor() {
    
        this.dom = {
          ui: document.querySelector( '.ui' ),
          game: document.querySelector( '.ui__game' ),
          back: document.querySelector( '.ui__background' ),
          prefs: document.querySelector( '.ui__prefs' ),
          theme: document.querySelector( '.ui__theme' ),
          stats: document.querySelector( '.ui__stats' ),
          texts: {
            title: document.querySelector( '.text--title' ),
            note: document.querySelector( '.text--note' ),
            timer: document.querySelector( '.text--timer' ),
            complete: document.querySelector( '.text--complete' ),
            best: document.querySelector( '.text--best-time' ),
            theme: document.querySelector( '.text--theme' ),
          },
          buttons: {
            prefs: document.querySelector( '.btn--prefs' ),
            back: document.querySelector( '.btn--back' ),
            stats: document.querySelector( '.btn--stats' ),
            reset: document.querySelector( '.btn--reset' ),
            theme: document.querySelector( '.btn--theme' ),
          },
        };
    
        this.world = new World( this );
        this.cube = new Cube( this );
        this.controls = new Controls( this );
        this.scrambler = new Scrambler( this );
        this.transition = new Transition( this );
        this.timer = new Timer( this );
        this.preferences = new Preferences( this );
        this.scores = new Scores( this );
        this.storage = new Storage( this );
        this.confetti = new Confetti( this );
        this.themes = new Themes( this );
        this.themeEditor = new ThemeEditor( this );
    
        this.initActions();
    
        this.state = STATE.Menu;
        this.newGame = false;
        this.saved = false;
    
        this.storage.init();
        this.preferences.init();
        this.cube.init();
        this.transition.init();
    
        this.storage.loadGame();
        this.scores.calcStats();
    
        setTimeout( () => {
    
          this.transition.float();
          this.transition.cube( SHOW );
    
          setTimeout( () => this.transition.title( SHOW ), 700 );
          setTimeout( () => this.transition.buttons( BUTTONS.Menu, BUTTONS.None ), 1000 );
    
        }, 500 );
    
      }
    
      initActions() {
    
        let tappedTwice = false;
    
        this.dom.game.addEventListener( 'click', event => {
    
          if ( this.transition.activeTransitions > 0 ) return;
          if ( this.state === STATE.Playing ) return;
    
          if ( this.state === STATE.Menu ) {
    
            if ( ! tappedTwice ) {
    
              tappedTwice = true;
              setTimeout( () => tappedTwice = false, 300 );
              return false;
    
            }
    
            this.game( SHOW );
    
          } else if ( this.state === STATE.Complete ) {
    
            this.complete( HIDE );
    
          } else if ( this.state === STATE.Stats ) {
    
            this.stats( HIDE );
    
          } 
    
        }, false );
    
        this.controls.onMove = () => {
    
          if ( this.newGame ) {
            
            this.timer.start( true );
            this.newGame = false;
    
          }
    
        };
    
        this.dom.buttons.back.onclick = event => {
    
          if ( this.transition.activeTransitions > 0 ) return;
    
          if ( this.state === STATE.Playing ) {
    
            this.game( HIDE );
    
          } else if ( this.state === STATE.Prefs ) {
    
            this.prefs( HIDE );
    
          } else if ( this.state === STATE.Theme ) {
    
            this.theme( HIDE );
    
          }
    
        };
    
        this.dom.buttons.reset.onclick = event => {
    
          if ( this.state === STATE.Theme ) {
    
            this.themeEditor.resetTheme();
    
          }
          
        };
    
        this.dom.buttons.prefs.onclick = event => this.prefs( SHOW );
    
        this.dom.buttons.theme.onclick = event => this.theme( SHOW );
    
        this.dom.buttons.stats.onclick = event => this.stats( SHOW );
    
        this.controls.onSolved = () => this.complete( SHOW );
    
      }
    
      game( show ) {
    
        if ( show ) {
    
          if ( ! this.saved ) {
    
            this.scrambler.scramble();
            this.controls.scrambleCube();
            this.newGame = true;
    
          }
    
          const duration = this.saved ? 0 :
            this.scrambler.converted.length * ( this.controls.flipSpeeds[0] + 10 );
    
          this.state = STATE.Playing;
          this.saved = true;
    
          this.transition.buttons( BUTTONS.None, BUTTONS.Menu );
    
          this.transition.zoom( STATE.Playing, duration );
          this.transition.title( HIDE );
    
          setTimeout( () => {
    
            this.transition.timer( SHOW );
            this.transition.buttons( BUTTONS.Playing, BUTTONS.None );
    
          }, this.transition.durations.zoom - 1000 );
    
          setTimeout( () => {
    
            this.controls.enable();
            if ( ! this.newGame ) this.timer.start( true );
    
          }, this.transition.durations.zoom );
    
        } else {
    
          this.state = STATE.Menu;
    
          this.transition.buttons( BUTTONS.Menu, BUTTONS.Playing );
    
          this.transition.zoom( STATE.Menu, 0 );
    
          this.controls.disable();
          if ( ! this.newGame ) this.timer.stop();
          this.transition.timer( HIDE );
    
          setTimeout( () => this.transition.title( SHOW ), this.transition.durations.zoom - 1000 );
    
          this.playing = false;
          this.controls.disable();
    
        }
    
      }
    
      prefs( show ) {
    
        if ( show ) {
    
          if ( this.transition.activeTransitions > 0 ) return;
    
          this.state = STATE.Prefs;
    
          this.transition.buttons( BUTTONS.Prefs, BUTTONS.Menu );
    
          this.transition.title( HIDE );
          this.transition.cube( HIDE );
    
          setTimeout( () => this.transition.preferences( SHOW ), 1000 );
    
        } else {
    
          this.cube.resize();
    
          this.state = STATE.Menu;
    
          this.transition.buttons( BUTTONS.Menu, BUTTONS.Prefs );
    
          this.transition.preferences( HIDE );
    
          setTimeout( () => this.transition.cube( SHOW ), 500 );
          setTimeout( () => this.transition.title( SHOW ), 1200 );
    
        }
    
      }
    
      theme( show ) {
    
        this.themeEditor.colorPicker( show );
        
        if ( show ) {
    
          if ( this.transition.activeTransitions > 0 ) return;
    
          this.cube.loadFromData( States[ '3' ][ 'checkerboard' ] );
    
          this.themeEditor.setHSL( null, false );
    
          this.state = STATE.Theme;
    
          this.transition.buttons( BUTTONS.Theme, BUTTONS.Prefs );
    
          this.transition.preferences( HIDE );
    
          setTimeout( () => this.transition.cube( SHOW, true ), 500 );
          setTimeout( () => this.transition.theming( SHOW ), 1000 );
    
        } else {
    
          this.state = STATE.Prefs;
    
          this.transition.buttons( BUTTONS.Prefs, BUTTONS.Theme );
    
          this.transition.cube( HIDE, true );
          this.transition.theming( HIDE );
    
          setTimeout( () => this.transition.preferences( SHOW ), 1000 );
          setTimeout( () => {
    
            const gameCubeData = JSON.parse( localStorage.getItem( 'theCube_savedState' ) );
    
            if ( !gameCubeData ) {
    
              this.cube.resize( true );
              return;
    
            }
    
            this.cube.loadFromData( gameCubeData );
    
          }, 1500 );
    
        }
    
      }
    
      stats( show ) {
    
        if ( show ) {
    
          if ( this.transition.activeTransitions > 0 ) return;
    
          this.state = STATE.Stats;
    
          this.transition.buttons( BUTTONS.Stats, BUTTONS.Menu );
    
          this.transition.title( HIDE );
          this.transition.cube( HIDE );
    
          setTimeout( () => this.transition.stats( SHOW ), 1000 );
    
        } else {
    
          this.state = STATE.Menu;
    
          this.transition.buttons( BUTTONS.Menu, BUTTONS.None );
    
          this.transition.stats( HIDE );
    
          setTimeout( () => this.transition.cube( SHOW ), 500 );
          setTimeout( () => this.transition.title( SHOW ), 1200 );
    
        }
    
      }
    
      complete( show ) {
    
        if ( show ) {
    
          this.transition.buttons( BUTTONS.Complete, BUTTONS.Playing );
    
          this.state = STATE.Complete;
          this.saved = false;
    
          this.controls.disable();
          this.timer.stop();
          this.storage.clearGame();
    
          this.bestTime = this.scores.addScore( this.timer.deltaTime );
    
          this.transition.zoom( STATE.Menu, 0 );
          this.transition.elevate( SHOW );
    
          setTimeout( () => {
    
            this.transition.complete( SHOW, this.bestTime );
            this.confetti.start();
    
          }, 1000 );
    
        } else {
    
          this.state = STATE.Stats;
          this.saved = false;
    
          this.transition.timer( HIDE );
          this.transition.complete( HIDE, this.bestTime );
          this.transition.cube( HIDE );
          this.timer.reset();
    
          setTimeout( () => {
    
            this.cube.reset();
            this.confetti.stop();
    
            this.transition.stats( SHOW );
            this.transition.elevate( 0 );
    
          }, 1000 );
    
          return false;
    
        }
    
      }
    
    }
    
    window.version = '0.99.2';
    window.game = new Game();

    In short, making an Animated Rubik’s Cube using HTML, CSS, and JavaScript is a cool way to learn about 3D design and animation on the web. It’s fun, colorful, and a great project to boost your front-end skills! 🎯🧩

    If your project has problems, don’t worry. Just click to download the source code and face your coding challenges with excitement. Have fun coding!

    Animation JavaScript
    Share. Copy Link Twitter Facebook LinkedIn Email WhatsApp
    Previous ArticleHow to Create Animated Bicycle Product Card using HTML & CSS
    Next Article How to create Impossible light bulb using HTML CSS and JS
    Coding Stella
    • Website

    Related Posts

    HTML & CSS

    How to create Social media popup hover menu using HTML and CSS

    25 June 2025
    JavaScript

    How to create Valentine’s Love Button Animation using HTML CSS and JS

    23 June 2025
    JavaScript

    How to create Magic Indicator Menu using HTML CSS and JS

    20 June 2025
    Add A Comment
    Leave A Reply Cancel Reply

    Trending Post

    Master Frontend in 100 Days Ebook

    2 March 202419K Views

    How to make Modern Login Form using HTML & CSS | Glassmorphism

    11 January 202417K Views

    How to make I love you Animation in HTML CSS & JavaScript

    14 February 202416K Views

    How to make Valentine’s Day Card using HTML & CSS

    13 February 202412K Views
    Follow Us
    • Instagram
    • Facebook
    • YouTube
    • Twitter
    ads
    Featured Post

    How to make 10 Different Pulsing loading effect using HTML & CSS

    23 January 2024

    10 Must-Have VSCode Extensions for Web Development 🛸

    25 January 2024

    HTML Cheat Sheet | Mastering HTML : Your Go-To Cheat Sheet

    31 January 2024

    How to make Awesome Search Bar using HTML & CSS

    14 January 2024
    Latest Post

    How to create Social media popup hover menu using HTML and CSS

    25 June 2025

    How to create Valentine’s Love Button Animation using HTML CSS and JS

    23 June 2025

    How to create Magic Indicator Menu using HTML CSS and JS

    20 June 2025

    How to create Awesome Cool Loading Animation using HTML CSS and JS

    16 June 2025
    Facebook X (Twitter) Instagram YouTube
    • About Us
    • Privacy Policy
    • Return and Refund Policy
    • Terms and Conditions
    • Contact Us
    • Buy me a coffee
    © 2025 Coding Stella. Made with 💙 by @coding.stella

    Type above and press Enter to search. Press Esc to cancel.

    Ad Blocker Enabled!
    Ad Blocker Enabled!
    Looks like you're using an ad blocker. We rely on advertising to help fund our site.
    Okay! I understood