Code Three.js in ActionScript with Randori Compiler

Posted on May 17, 2013 | 8 comments

Live Demo

Live Demo of three.js html/js generated by ActionScript 3 using the Randori compiler – http://renaun.com/html5/as3threejs/

What is Randori?

Randori ProjectRandori is a project with various parts and can be found at http://randoriframework.com/. For my example I am using a subset of the Randori project’s capabilities, which is fine as the Randori project is put together in such a way that it allows for this. You can find the various Randori repos here: https://github.com/RandoriAS

Repos:

  • randori-sdk: This repo contains all the Randori framework bits you need for both the ActionScript 3 and JavaScript side.
  • randori-compiler: This repo contains the ActionScript 3 to JavaScript compiler source files. Precompiled version is located here
  • randori-framework & randori-guice-framework: These repos contains the source files for the Randori framework.
  • randori-libraries: This repo contains source files for ActionScript 3 classes that stubbed out JavaScript libraries. For example to get three.js code hinting in ActionScript there are stubbed out ActionScript 3 classes that map to three.js classes.
  • randori-plugin-intellij: This repo contains an IntelliJ plugin for creating Randori framework based apps and an integrated compiler.
  • randori-demos-bundle: This repo contains demo Randori applications.
  • randori-tools: This repo contains the Randori Toolset for ActionScript. These tools help to generate the JavaScript interop libraries.

The ActionScript three.js example in this post makes use of the randori-compiler, randori-libraries, and randori-sdk repos.

Setup Randori for Compiling ActionScript

Here are the steps I used to set up my development space:

  • Create a folder called RandoriRepos somewhere on your hard drive, and keep track of this folder path for later use.
  • Download http://randoriframework.com/nightly-builds/ to the RandoriRepos folder. Unzip the folder, you should see the RandoriRepos/randori-compiler-latest/ folder now.
  • Use a git client to clone the randori-libraries repo into the RandoriRepos folder. Or on the command line navigate to the RandoriRepos folder and execute:
    git clone git://github.com/RandoriAS/randori-libraries.git
  • Use a git client to clone the randori-sdk repo into the RandoriRepos folder. Or on the command line navigate to the RandoriRepos folder and execute:
    git clone git://github.com/RandoriAS/randori-sdk.git

The compiler is located at RandoriRepos/randori-compiler-latest/randori.jar. Next we’ll compile a simple ActionScript 3 class into JavaScript with the compiler.

Compiling ActionScript to JavaScript with the Randori Compiler

The sample files that I used to create the three.js example in ActionScript 3 are located at https://github.com/renaun/RandoriExamples.

  • Use a git client to clone the RandoriExamples repo into the parent folder of the RandoriRepos folder. Or on the command line navigate to the RandoriRepos’ parent folder and execute:
    git clone https://github.com/renaun/RandoriExamples.git

    We want RandoriRepos and RandoriExamples folders to have the same parent folder.

  • Open a command line and navigate to the RandoriExamples/SimpleClass folder.
  • Compile the HelloWorld.as file using the provided randori.sh script or with the following command modified with your path values:
    java -jar /path/to/RandoriRepos/randori-compiler-latest/randori.jar     randori.compiler.clients.Randori \
        -library-path /path/to/RandoriRepos/randori-sdk/randori-framework/bin/swc/builtin.swc \
        -source-path ./ \
        -js-classes-as-files=true \
        -output ./randori-output
  • You should see a HelloWorld.js file in the RandoriExamples/SimpleClass/randori-output/ folder. The contents should look like this:

    /** Compiled by the Randori compiler v0.2.4.2 on Thu May 16 15:47:28 PDT 2013 */


    HelloWorld = function() {
       
    };

    HelloWorld.prototype.toString = function() {
        return "Hello Randori!";
    };

    HelloWorld.className = "HelloWorld";

    HelloWorld.getClassDependencies = function(t) {
        var p;
        return [];
    };

    HelloWorld.injectionPoints = function(t) {
        return [];
    };

    To see the HelloWorld.js class in action take a look at RandoriExamples/SimpleClass/index.html.

    Well compiling a simple class is not that impressive. Lets get to the real example and show how to use ActionScript 3 and the Randori compiler to create a three.js demo

    NOTE: The Randori compiler will not magically convert any of the Flash Player APIs in ActionScript. Whatever you are coding in ActionScript 3 has to be available on the JavaScript side. Meaning we are writing ActionScript 3 code that will map to three.js APIs.

    Creating the three.js demo in ActionScript 3

    The first thing we need is the ability to reference some basic HTML DOM APIs and the three.js library in ActionScript 3. This is done with stub classes in the randori-libraries repo. But to use them with the Randori compiler we need to create a SWC version of the libraries we want to use. The SWC also allows for code hinting in your IDE, for example in Adobe Flash Builder.

    Here are the steps to setup the ActionScript Library Project in Adobe Flash Builder for the randori-libraries:

    • In Adobe Flash Builder click on File -> New -> ActionScript Library Project
    • Give it a project name of: randori-libraries
    • Set the Folder location to: /path/to/RandoriRepos/randori-libraries
    • Click Finish
    • Open the randori-libraries project properties by right clicking on the project -> Properties
    • Open ActionScript Library Build Path -> Library Path and remove the AIR SDK path library
    • In Library Path click on “Add SWC…”, enter the following path for builtin.swc and click Ok:
      ${PARENT-1-PROJECT_LOC}/randori-sdk/randori-framework/bin/swc/builtin.swc
    • Open up ActionScript Library Build Path -> Source Path and click on Add Folder, type in the following path and click OK:
      HTMLCoreLib/src
    • Repeat the above step for:
      ThreeJs/src 

    NOTE: The randori-sdk repo does contain compiled SWC versions for some of the randori-libraries. If you look at https://github.com/RandoriAS/randori-sdk/tree/master/randori-framework/bin/swc you can see it contains: HTMLCoreLib.swc, JQuery.swc, builtin.swc, randori-framework.swc, randori-guice-framework.swc.

    Now that we have the libraries for our stub classes we'll use this in the Randori compiler's -library-path arguments.

    You might be able to import the ActionScriptThreeJS Adobe Flash Builder project as is; the project is located in the RandoriExamples folder. The steps below can be skipped if the import works; these steps show how to create a new ActionScript project in Adobe Flash Builder and use the Randori stub libraries.

    • In Adobe Flash Builder click File -> New -> ActionScript Project
    • Give it a project name of: ActionScriptThreeJS
    • Set the Folder location to: /path/to/RandoriExamples/ActionScriptThreeJS
    • Click Finish
    • Open the ActionScriptThreeJS project properties by right clicking on the project -> Properties
    • Open ActionScript Build Path -> Library Path and remove the AIR SDK path library
    • In Library Path click on "Add Project...", select the "randori-libraries" project and click OK
    • Open ActionScript Compiler and uncheck the "Generate HTML wrapper file" checkbox
    • Open the src/ActionScriptThreeJS.as and you should see code hinting for Three class as well as others.

    Here is the code for ActionScriptThreeJS.as:

    /***
     *  Author: Renaun Erickson (renaun.com - @renaun - github.com/renaun)
     *  
     *  Attribution: Leonard Souza - https://github.com/leonardsouza/RandoriAS-ThreeJS
     */

    package
    {
    import com.mrdoob.stats.Stats;
    import com.mrdoob.three.Three;
    import com.mrdoob.three.cameras.PerspectiveCamera;
    import com.mrdoob.three.extras.geometries.CubeGeometry;
    import com.mrdoob.three.extras.geometries.PlaneGeometry;
    import com.mrdoob.three.loaders.ImageUtils;
    import com.mrdoob.three.materials.MeshBasicMaterial;
    import com.mrdoob.three.math.Matrix4;
    import com.mrdoob.three.math.Plane;
    import com.mrdoob.three.objects.Mesh;
    import com.mrdoob.three.renderers.WebGLRenderer;
    import com.mrdoob.three.scenes.Scene;
    import com.mrdoob.three.textures.Texture;

    import randori.webkit.dom.DomEvent;
    import randori.webkit.dom.Element;
    import randori.webkit.dom.MouseEvent;
    import randori.webkit.dom.TouchEvent;
    import randori.webkit.html.HTMLDivElement;
    import randori.webkit.html.HTMLElement;
    import randori.webkit.page.Window;

    import utils.RequestAnimationFrame;

    public class ActionScriptThreeJS
    {
        protected var container:Element;
        protected var stats:Stats;
       
        protected var camera:PerspectiveCamera;
        protected var scene:Scene;
        protected var renderer:WebGLRenderer;
       
        protected var cube:Mesh;
        protected var plane:Plane;
       
        protected var targetRotation:Number = 0;
        protected var targetRotationOnMouseDown:Number = 0;
       
        protected var mouseX:Number = 0;
        protected var mouseXOnMouseDown:Number = 0;
        protected var windowHalfX:Number = window.innerWidth / 2;
        protected var windowHalfY:Number = window.innerHeight / 2;
       
        [Inject]
        public var animation:RequestAnimationFrame;
       
        public function main():void
        {      
            container = new HTMLDivElement();
            window.document.body.appendChild(container);
           
            var info:HTMLElement = new HTMLDivElement();
            info.style.position ="absolute;"
            info.style.top ="10px";
            info.style.width ="100%";
            info.style.textAlign ="center";
            info.innerHTML ="Drag to spin the cube";
            container.appendChild(info);
           
            camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
            camera.position.y = 150;
            camera.position.z = 600;
           
            scene = new Scene();
           
            // FLOOR
            var floorTexture:Texture = ImageUtils.loadTexture("images/checkerboard.jpg");
            floorTexture.wrapS = floorTexture.wrapT = Three.RepeatWrapping;
            floorTexture.repeat.set(5, 5);
            var floorMaterial:MeshBasicMaterial = new MeshBasicMaterial({ map: floorTexture, side: Three.DoubleSide });
            var floorGeometry:PlaneGeometry = new PlaneGeometry(1200, 1200, 20, 20);
            var floor:Mesh = new Mesh(floorGeometry, floorMaterial);
            floor.position.y = -0.5;
            floor.rotation.x = Math.PI / 2;
            floor.position.z = -10;
            scene.add(floor);
           
            // Cube
            var geometry:CubeGeometry = new CubeGeometry(200, 200, 200);
            for (var i:int = 0; i < geometry.faces.length; i ++)
                geometry.faces[i].color.setHex(Math.random() * 0xffffff);
            var material:MeshBasicMaterial = new MeshBasicMaterial({ vertexColors: Three.FaceColors });
            cube = new Mesh(geometry, material);
            cube.position.y = 150;
            scene.add(cube);
           
            // Plane
            var geometry2:PlaneGeometry = new PlaneGeometry(200, 200);
            geometry2.applyMatrix(new Matrix4().makeRotationX(- Math.PI / 2 ));
            material = new MeshBasicMaterial({ color: 0xe0e0e0 });
            plane = new Mesh(geometry2, material) as Plane;
            scene.add(plane);
           
            renderer = new WebGLRenderer( {antialias:true} );
            renderer.setSize(window.innerWidth, window.innerHeight);
           
            container.appendChild(renderer.domElement);
           
            stats = new Stats();
            stats.domElement.style.position ="absolute";
            stats.domElement.style.top ="0px";
            container.appendChild(stats.domElement);
           
            window.document.addEventListener("mousedown", onDocumentMouseDown, false);
            window.document.addEventListener("touchstart", onDocumentTouchStart, false);
            window.document.addEventListener("touchmove", onDocumentTouchMove, false);
           
            window.addEventListener("resize", onWindowResize, false);
           
            startRender();
        }
       
       
        private function onDocumentMouseDown(event:MouseEvent):void
        {
            event.preventDefault();
           
            window.console.log("down");
            window.document.addEventListener("mousemove", onDocumentMouseMove, false);
            window.document.addEventListener("mouseup", onDocumentMouseUp, false);
            window.document.addEventListener("mouseout", onDocumentMouseOut, false);
           
            mouseXOnMouseDown = event.clientX - windowHalfX;
            targetRotationOnMouseDown = targetRotation;
        }
       
        private function onDocumentTouchStart(event:TouchEvent):void
        {
            if (event.touches.length === 1) {
               
                event.preventDefault();
               
                mouseXOnMouseDown = event.touches[0].pageX - windowHalfX;
                targetRotationOnMouseDown = targetRotation;
            }
        }
       
        private function onDocumentTouchMove(event:TouchEvent):void
        {
            if (event.touches.length === 1) {
               
                event.preventDefault();
               
                mouseX = event.touches[0].pageX - windowHalfX;
                targetRotation = targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.05;
                window.console.log("touchMove");
            }
        }
       
        private function onDocumentMouseMove(event:MouseEvent):void
        {
            mouseX = event.clientX - windowHalfX;
            targetRotation = targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown ) * 0.02;
        }
       
        private function onDocumentMouseUp(event:MouseEvent):void
        {
            window.console.log("up");      
            window.document.removeEventListener("mousemove", onDocumentMouseMove, false);
            window.document.removeEventListener("mouseup", onDocumentMouseUp, false);
            window.document.removeEventListener("mouseout", onDocumentMouseOut, false);
        }
       
        private function onDocumentMouseOut(event:MouseEvent):void
        {
            window.console.log("out");
            window.document.removeEventListener("mousemove", onDocumentMouseMove, false);
            window.document.removeEventListener("mouseup", onDocumentMouseUp, false);
            window.document.removeEventListener("mouseout", onDocumentMouseOut, false);
        }
       
        private function onWindowResize(event:DomEvent):void
        {
            windowHalfX = window.innerWidth / 2;
            windowHalfY = window.innerHeight / 2;
           
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
           
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
       
        private function startRender():void
        {
            animation.request(startRender);
           
            render();
            stats.update();
        }
       
        private function render():void
        {
            plane.rotation.y = cube.rotation.y += (targetRotation - cube.rotation.y) * 0.05;
            renderer.render(scene, camera);
        }
    }
    }

    The class contains a main() method that is called by the GuiceInjectorBootstrap class once the class has been instantiated. We'll talk about the bootstrap class in a bit. In the main() method you can see three.js like classes. For example the "//Floor" code was copied straight out of a JavaScript example with minor changes to make it reference class names directly without the JavaScript THREE. prefix (ie: ImageUtils instead of THREE.ImageUtils) and strong typing declarations.

    The ActionScript class in the randori-libraries/ThreeJS folder are not guaranteed to be complete and are based on r57 of three.js. But it is enough to show you how to mock up a 3rd party JS library and use it in ActionScript 3 with randori-compiler. It was easy to add missing stuff too, I added the ImageUtils class in a jiffy and had it working with r58 of three.js easily.

    Well enough talk we need to compile the ActionScript into JavaScript and see it run. Lets take a look at the compile arguments for this project. There is a shell script that shows the compiler arguments, if you are on a Mac you can easily run it in terminal. Here is the full command; this needs to be run in the command line from the RandoriExamples/ActionScriptThreeJS/src/ folder:

    java -jar /path/to/RandoriRepos/randori-compiler-latest/randori.jar randori.compiler.clients.Randori \
        -sdk-path=/path/to/RandoriRepos/randori-sdk/ \
        -library-path /path/to/RandoriRepos/randori-libraries/bin/randori-libraries.swc \
        -source-path ./ \
        -js-classes-as-files=true \
        -output ../js-randori

    The -sdk-path will tell the compiler where the randori-sdk is and will copy over the required randori js files. The -sdk-path also will link in the swc files from the randori-sdk location, which means we don't have to use the -library-path argument to add builtin.swc, HTMLCoreLib.swc, or the randori-guice-framework.swc (Used by the GuiceInjectorBootstrap.as class). The -library-path references the needed SWC's to compile the ActionScript files. The rest should be straightforward, but here is a link to the documentation for the randori compiler arguments.

    The code generated by the Randori compiler is found in RandoriExamples/ActionScriptThreeJS/js-randori. It includes the following files:

    • GuiceInjectorBootstrap.js
    • ActionScriptThreeJS.js
    • randori-framework-min.js
    • randori-framework.js
    • randori-guice-framework-min.js
    • randori-guice-framework.js
    • utils/RequestAnimationFrame.js

    The current Randori compiler doesn't create a wrapper HTML file but I have put in a request for it to create a basic one. Either way we still have to hook up the three.js library and other application assets. These files are located in the RandoriExamples/ActionScriptThreeJS folder as the following:

    • index.html - The three.js example index html file.
    • images/checkerboard.jpg - Asset file used in the three.js demo.
    • js/ - contains the three.js and stats.js libraries that are included in the index.html

    Here is what the index.html file looks like:

    <!DOCTYPE HTML>
    <html>
    <head>
        <title>ActionScript to ThreeJS Example</title>
        <script src="js/three.min.js"></script>
        <script src="js/stats.min.js"></script>
        <script src="js-randori/randori-guice-framework.js"></script>
        <script src="js-randori/GuiceInjectorBootstrap.js"></script>
    </head>
    <body>
        <script>
            onload = function() {
                var bootstrap = new GuiceInjectorBootstrap( "ActionScriptThreeJS", "js-randori/" );
                bootstrap.launch();
            }
        </script>
    </body>
    </html>

    So here is where some of the Randori magic happens. The Randori compiler outputs code that the randori-guice-framework.js can do stuff with. What kind of stuff? Well, like dependency injection and lazy loading compiled JavaScript classes when they are needed. The GuiceInjectorBootstrap.as file contains Randori guice classes to handle the loading and injection. All we have to do is pass in the main class file and output folder where the generated JavaScript is located. In this example this is done by the var bootstrap = new GuiceInjectorBootstrap( "ActionScriptThreeJS", "js-randori/" ); code. It loads the js-randori/ActionScriptThreeJS.js file. When it finds that ActionScriptThreeJS is looking for utils/RequestAnimationFrame.js from the [Inject] metadata used in the ActionScriptThreeJS.as file it will go load that class file too. To demonstrate this, run the index.html in the browser and check the network calls in Chrome Developer Tools. It should look something like this:
    threejsdemonetworkcalls

    You can see as it loads three.js, stats.js, randori-guice-framework.js and GuiceInjectorBootstrap.js they are parallel calls. Then it loads the class files, first the ActionScriptThreeJS.js file (which is not included in the index.html script tags). The Randori guice framework then notices that ActionScriptThreeJS injects the utils.RequestAnimationFrame classes and goes and fetches that JavaScript class file. So no worrying about what classes are required to run your code just give it the main class and let Randori guice does the rest.

    This is actually where the full Randori framework is pretty cool. The full Randori stack is geared for large applications making use of MVC patterns and dependency injection for some great features. A great place to learn more about the Randori framework for large application development is the getting started section here.

    Randori is a fairly new project but it was really fun to play with and something I'll be exploring more.

    Links Used in the Post

    RandoriAS Project Github - https://github.com/RandoriAS
    My ActionScript 3 Three.js Example Code - https://github.com/renaun/RandoriExamples
    My ActionScript 3 Three.js Live Demo - http://renaun.com/html5/as3threejs/

    A thank you goes out to Leonard Souza for providing the ThreeJS+Randori example - https://github.com/leonardsouza/RandoriAS-ThreeJS

    • Jeff Ward

      Cool post, Renaun, I’ll have to check this tech out…

      Randori looks interesting, but their “What is Randori” page failed epically in giving me any sense of what Randori actually is.

      They began with trivia about the origin of the word, followed by a page-long bulleted list of random crap like “You want an application that is testable in units.” – great, who doesn’t… But WHAT THE CRAP IS RANDORI?!

      Worst product brief _ever_

      I’d yell at them on Twitter but I don’t use Twitter. So I hope they find my rant here.

      • renaun

        It is a open source community project, a little hard to explain in a brief. Hopefully my post will explain some of the possibilities of Randori, there is more to it though.
        Sent from a device

        • supritha

          Hi, I am Supritha. I have a action script to identify installed Adobe air application on desktop…Is it possible to port this action script code to javascript?

          We don’t want unnecessary flash plugin to excecute action script file…Can this code directly contact Adobe air application?

          • renaun

            Not sure depends on what API you are using to identify the AIR application.

      • Roland Zwaga

        Hi Jeff,

        perhaps reading through the getting started pages will give you a better notion of what we’re about:

        https://github.com/RandoriAS/randori-plugin-intellij/wiki/_pages

        thank you for your comments on the general ‘What is Randori’ page, we’ll try and improve that in the future.

        cheers,

        Roland

      • http://twitter.com/mlabriola Michael Labriola

        Hey Jeff.

        Thanks for the rant. You are write. We will get something better there shortly. I hope you do check it out and, if you like it, consider looking over the rest of our content and giving us feedback or changing as you see fit.

        Thanks for taking the time to mention,

        Mike

      • http://twitter.com/webDoubleFx Frédéric THOMAS

        @google-71ec7895cada78741057c644d858b0e3:disqus You’re right, it needs to be improved, thanks for telling us

        @renaun:disqus Great post man, happy you enjoyed it :)

      • Roland Zwaga

        FYI, we have updated the “What is” page with, what we consider, a better explanation of the framework and its various components and purposes:

        http://randoriframework.com/what_is_randori/

    • Pingback: Philippe.me » Type annotations for the CreateJS Toolkit

    • Pingback: randori?ActionScript?JavaScript????????framework « ????TV??