FreeFallMenu by Maciej Plesnar | JavaScriptSource

FreeFallMenu by Maciej Plesnar

Maciej Ple??nar Mar 16, 2016

Abstract

Cool drop-down 3d menu effect with real physics in css and javascript.

Description

This drop-down menu widget uses matter.js physics engine for realistic simulation. Written in JS and CSS 3D (extensive use of transformations).

Code Snippet

function radToDeg(num) {
  return num*180/Math.PI;
}

(function() {

    var MyRenderer = {
      create: function() {
        return { controller: MyRenderer };
      },
      world: function(engine) {
        var cumulativeHeight = 0;
        for (var i=0; i<bodies.length; i++) {
          cumulativeHeight += bodies[i].height
          bodies[i].domelement.style.transform = 'translate3d(0px, '+(bodies[i].position.y-cumulativeHeight+(bodies[i].height/2)-startY)+'px, '+(-bodies[i].position.x)+'px) rotateX('+(radToDeg(bodies[i].angle)-90)+'deg)';
        }
      },
      clear: function(engine) {
      }
    };

    // Matter aliases
    var Engine = Matter.Engine,
        World = Matter.World,
        Bodies = Matter.Bodies,
        Body = Matter.Body,
        Composite = Matter.Composite,
        Composites = Matter.Composites,
        Common = Matter.Common,
        Constraint = Matter.Constraint,
        RenderPixi = Matter.RenderPixi,
        Events = Matter.Events,
        Bounds = Matter.Bounds,
        Vector = Matter.Vector,
        Vertices = Matter.Vertices,
        MouseConstraint = Matter.MouseConstraint,
        Mouse = Matter.Mouse,
        Query = Matter.Query;

    var FreeFallMenu = {};

    var _engine,
        _sceneEvents = [],
        bodies = [],
        endConstraint,
        on = false

    FreeFallMenu.init = function() {

        // Uncomment for debug mode
        var container = document.getElementById('freefallmenu-container');

        var options = {
            positionIterations: 6,
            velocityIterations: 4,
            render: {
              controller: MyRenderer
            }
        };

        _engine = Engine.create(container, options);
        Engine.run(_engine);

        var _world = _engine.world;

        FreeFallMenu.reset();

        var groupId = Matter.Body.nextGroupId();
        var menuElements = document.getElementsByClassName('freefallmenu-element');
        var cumulativeHeight = 0;

        //Bodies
        for (var i=0;i<menuElements.length;i++) {
          var height = menuElements[i].offsetHeight;
          bodies[i] = Bodies.rectangle(0, startY, height, 2.0, {groupId: groupId});
          bodies[i].height = height;
          bodies[i].domelement = menuElements[i];
          cumulativeHeight += bodies[i].height;
          Matter.Body.rotate(bodies[i],-startAngle);
          Matter.Body.translate(bodies[i],{x: (cumulativeHeight-height/2)*Math.cos(startAngle), y: -(cumulativeHeight-height/2)*Math.sin(startAngle)});
          World.add(_world, bodies[i]);
        }

        //Constraints
        var worldPositionConstraintX = (bodies[0].height/2-0.5)*Math.cos(startAngle);
        var worldPositionConstraintY = (bodies[0].height/2-0.5)*Math.sin(startAngle);
        World.add(_world, Constraint.create({
            pointA: { x: 0, y: startY },
            pointB: { x: -worldPositionConstraintX, y: worldPositionConstraintY },
            bodyB: bodies[0],
            stiffness: 1,
        }));
        for (i=0;i<menuElements.length-1;i++) {
          World.add(_world, Constraint.create({
            pointA: { x: (bodies[i].height/2-0.5)*Math.cos(startAngle), y: -(bodies[i].height/2-0.5)*Math.sin(startAngle) },
            pointB: { x: -(bodies[i+1].height/2-0.5)*Math.cos(startAngle), y: (bodies[i+1].height/2-0.5)*Math.sin(startAngle) },
            bodyA: bodies[i],
            bodyB: bodies[i+1],
            stiffness: 1,
          }));
        }
        endConstraint = Constraint.create({
            pointA: { x: cumulativeHeight*Math.cos(startAngle), y: startY-cumulativeHeight*Math.sin(startAngle) },
            pointB: { x: (bodies[bodies.length-1].height/2-0.5)*Math.cos(startAngle), y: -(bodies[bodies.length-1].height/2-0.5)*Math.sin(startAngle) },
            bodyB: bodies[bodies.length-1],
            stiffness: 1,
            length: 0.01,
            angularStiffness: 1,
            render: {
                strokeStyle: '#90EE90',
                lineWidth: 3
            }
        });
        World.add(_world,endConstraint);

        _sceneEvents.push(

            Events.on(_engine, 'beforeUpdate', function(event) {
              if (!on) {
                var engine = event.source;
                var rotX = (bodies[bodies.length-1].height-0.5)*Math.cos(bodies[bodies.length-1].angle)/2;
                var rotY = (bodies[bodies.length-1].height-0.5)*Math.sin(bodies[bodies.length-1].angle)/2;
                var endPoint = Matter.Vector.add(bodies[bodies.length-1].position, {x:rotX, y:rotY});
                var cumulativeHeight = 0;
                for (i=0;i<bodies.length;i++) {
                  cumulativeHeight+=bodies[i].height
                }
                var dest = { x: cumulativeHeight*Math.cos(startAngle), y: startY-cumulativeHeight*Math.sin(startAngle) };
                var dist = Matter.Vector.magnitude(Matter.Vector.sub(dest, endPoint));
                var normal = Matter.Vector.normalise(Matter.Vector.sub(dest, endPoint));
                var vectorToMove = Matter.Vector.add(endPoint, {x:(normal.x*Math.max(dist/50,1)), y:(normal.y*Math.max(dist/70,1))});
                if (dist > 1) {
                  // Moving the menu upwards
                  endConstraint.pointA = vectorToMove;
                }
              }
            })

        );

        // Events
        var dropdownButton = document.getElementById('freefallmenu-button');
        if (dropdownButton.addEventListener) {
            dropdownButton.addEventListener('mouseover', FreeFallMenu.mouseOver);
            dropdownButton.addEventListener('mouseout', FreeFallMenu.mouseOut);
        } else if (dropdownButton.attachEvent) {
            dropdownButton.attachEvent('mouseover', FreeFallMenu.mouseOver);
            dropdownButton.attachEvent('mouseout', FreeFallMenu.mouseOut);
        }
        for (var i=0;i<menuElements.length;i++) {
          var menuElement = menuElements[i];
          if (menuElement.addEventListener) {
              menuElement.addEventListener('mouseover', FreeFallMenu.mouseOver);
              menuElement.addEventListener('mouseout', FreeFallMenu.mouseOut);
          } else if (menuElement.attachEvent) {
              menuElement.attachEvent('mouseover', FreeFallMenu.mouseOver);
              menuElement.addEventListener('mouseout', FreeFallMenu.mouseOut);
          }
        }

    };

    FreeFallMenu.mouseOver = function() {
        on = true;
        endConstraint.pointA = null;
    };

    FreeFallMenu.mouseOut = function() {
        on = false;
    };

    if (window.addEventListener) {
        window.addEventListener('load', FreeFallMenu.init);
    } else if (window.attachEvent) {
        window.attachEvent('load', FreeFallMenu.init);
    }

    var startAngle = 50 * Math.PI/180;
    var startY = 200;

    FreeFallMenu.reset = function() {

        var _world = _engine.world;

        World.clear(_world);
        Engine.clear(_engine);

        var renderController = _engine.render.controller;
        if (renderController.clear)
            renderController.clear(_engine.render);

        for (var i = 0; i < _sceneEvents.length; i++)
            Events.off(_engine, _sceneEvents[i]);
        _sceneEvents = [];

        Common._nextId = 0;

        _engine.enableSleeping = true;

        _mouseConstraint = MouseConstraint.create(_engine);
        World.add(_world, _mouseConstraint);

    };

})();

Download

Download

Leave a Response

(0 comments)