How can Khan Academy computer programs be run offline or on my own website?
Asked Answered
R

4

13

I have developed programs in Khan Academy's Computer Programming lessons that I would like to run outside of Khan Academy. How can that be done?

Respond answered 16/8, 2014 at 15:21 Comment(0)
R
17

Khan Academy uses Processing.js, a JavaScript library for interacting with <canvas> elements. Although Processing is actually a language in its own right, Khan Academy uses JavaScript-only Processing.js code.

So you need to set up a web page that imports Processing.js, sets up a <canvas>, and builds a Processing.js instance on the canvas. Finally you need to make sure your Khan Academy code has all the members of the Processing.js instance in scope (I do this with with), plus some equivalent of Khan Academy's small modifications to Processing.js, like mouseIsPressed and getImage.

Here is some boilerplate that has been working for me. Probably further development will be required to get it working for more complicated examples; please post comments when you find examples that don't work.

<!DOCTYPE html>
<html>
<head>
  <title>JavaScript</title>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/processing.js/1.4.8/processing.min.js"></script>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script>
    var canvas = document.getElementById("canvas");
    var processing = new Processing(canvas, function(processing) {
        processing.size(400, 400);
        processing.background(0xFFF);

        var mouseIsPressed = false;
        processing.mousePressed = function () { mouseIsPressed = true; };
        processing.mouseReleased = function () { mouseIsPressed = false; };

        var keyIsPressed = false;
        processing.keyPressed = function () { keyIsPressed = true; };
        processing.keyReleased = function () { keyIsPressed = false; };

        function getImage(s) {
            var url = "https://www.kasandbox.org/programming-images/" + s + ".png";
            processing.externals.sketch.imageCache.add(url);
            return processing.loadImage(url);
        }

        // use degrees rather than radians in rotate function
        var rotateFn = processing.rotate;
        processing.rotate = function (angle) {
            rotateFn(processing.radians(angle));
        };

        with (processing) {


            // INSERT YOUR KHAN ACADEMY PROGRAM HERE


        }
        if (typeof draw !== 'undefined') processing.draw = draw;
    });
  </script>
</body>
</html>
Respond answered 16/8, 2014 at 15:21 Comment(5)
Could you post this on gist.github.com and write a license preamble so I can use it in my own code?Marable
I believe that by virtue of being posting on Stack Overflow it is licensed under CC BY-SA. Does that work for you?Respond
I was aware of that, but I don't like assuming others are aware of that clause. Thanks for your permission :)Marable
This works perfectly for most of the programs I've tried. A few of them were animating far too quickly but hopefully I'll be able to figure out how to fix that.Underling
@thinsoldier: I guess the too fast animation comes from the fact that processing.js deals with radians and Khan Academy with degrees. If you switch in Khan Academy to radians mode (angleMode = "radians";) than it works fine.Regale
R
3

ADDON to the answer of Robert:

Processing.js uses radians as default for angle values, Khan Academy JS uses degrees. If you add following lines to Robert's code above (before the with statement), then you can use the rotate commands as they come from KA.

var rotateFn = processing.rotate;
processing.rotate = function(angle) {
    rotateFn(processing.radians(angle));
}
Regale answered 19/10, 2015 at 12:35 Comment(0)
W
2

I have created a script that solves exactly this problem. You can read its documentation in the comments that are at the top of its .js file https://github.com/vExcess/libraries/blob/main/runPJS.js

Here is it's most basic usage:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Using PJS in HTML</title>
</head>
<body>

    <script class="pjs-src" type="data">
        // WRITE PROCESSING.JS CODE HERE
        background(255, 0, 0);
    </script>
  
    <!-- import my script which will automatically run the PJS code -->
    <script src="https://cdn.jsdelivr.net/gh/vExcess/libraries@main/runPJS.js"></script>

</body>
</html>

You can see a demo of it in use here: https://www.khanacademy.org/computer-programming/how-to-use-easily-use-processingjs-in-html/6388181215264768

My script will run Khan Academy's version of PJS meaning functions such as getImage() will work (except some images refuse to load for an unknown reason) and also angleMode defaults to degrees. It also solves the images loading asynchronously for you. You can run almost any program from Khan Academy without making any alterations to the code. I will note that it currently doesn't support getSound/playSound from KA.

Whole answered 6/10, 2022 at 6:42 Comment(0)
U
1

Yeah, sorry I'm kind-of late to this question. But I have figured it out on my own...

You can clone this repository or follow the instructions down below:

Or you can set things up like this: In your index.html put source to all 3 of these files:

Processing.js:

https://raw.githubusercontent.com/Khan/processing-js/66bec3a3ae88262fcb8e420f7fa581b46f91a052/processing.js

loadKa.js:

This is production code to load multiple separate files together:

!(function(window, JSON, localStorage)
{
    function createProcessing()
    {
        var args = Array.prototype.slice.call(arguments);
        args.push({ beginCode: "with(processing)\n{", endCode: "}"});
        var any = combine.apply(this, args);

        this.cache = window.cache = {};
        this.cache.loadedImages = window.cache.loadedImages = {};
        this.cache.imageNames = window.cache.imageNames = [
            "avatars/aqualine-sapling", 
            "avatars/aqualine-seed", 
            "avatars/aqualine-seedling", 
            "avatars/aqualine-tree", 
            "avatars/aqualine-ultimate", 
            "avatars/avatar-team", 
            "avatars/duskpin-sapling", 
            "avatars/duskpin-seed", 
            "avatars/duskpin-tree", 
            "avatars/duskpin-ultimate", 
            "avatars/leaf-blue", 
            "avatars/leaf-green", 
            "avatars/leaf-grey", 
            "avatars/leaf-orange", 
            "avatars/leaf-red", 
            "avatars/leaf-yellow", 
            "avatars/leafers-sapling", 
            "avatars/leafers-seed", 
            "avatars/leafers-seedling", 
            "avatars/leafers-tree", 
            "avatars/leafers-ultimate", 
            "avatars/marcimus", 
            "avatars/marcimus-orange", 
            "avatars/marcimus-purple", 
            "avatars/marcimus-red", 
            "avatars/mr-pants", 
            "avatars/mr-pants-green", 
            "avatars/mr-pants-orange", 
            "avatars/mr-pants-pink", 
            "avatars/mr-pants-purple", 
            "avatars/mr-pants-with-hat", 
            "avatars/mr-pink", 
            "avatars/mr-pink-green", 
            "avatars/mr-pink-orange", 
            "avatars/old-spice-man", 
            "avatars/old-spice-man-blue", 
            "avatars/orange-juice-squid", 
            "avatars/piceratops-sapling", 
            "avatars/piceratops-seed", 
            "avatars/piceratops-seedling", 
            "avatars/piceratops-tree", 
            "avatars/piceratops-ultimate", 
            "avatars/primosaur-sapling", 
            "avatars/primosaur-seed", 
            "avatars/primosaur-seedling", 
            "avatars/primosaur-tree", 
            "avatars/primosaur-ultimate", 
            "avatars/purple-pi", 
            "avatars/purple-pi-pink", 
            "avatars/purple-pi-teal", 
            "avatars/questionmark", 
            "avatars/robot_female_1", 
            "avatars/robot_female_2", 
            "avatars/robot_female_3", 
            "avatars/robot_male_1", 
            "avatars/robot_male_2", 
            "avatars/robot_male_3", 
            "avatars/spunky-sam", 
            "avatars/spunky-sam-green", 
            "avatars/spunky-sam-orange", 
            "avatars/spunky-sam-red", 
            "avatars/starky-sapling", 
            "avatars/starky-seed", 
            "avatars/starky-seedling", 
            "avatars/starky-tree", 
            "avatars/starky-ultimate", 
            "creatures/Hopper-Happy", 
            "creatures/Hopper-Cool", 
            "creatures/Hopper-Jumping", 
            "creatures/OhNoes", 
            "creatures/OhNoes-Happy", 
            "creatures/OhNoes-Hmm", 
            "cute/Blank", 
            "cute/BrownBlock", 
            "cute/CharacterBoy", 
            "cute/CharacterCatGirl", 
            "cute/CharacterHornGirl", 
            "cute/CharacterPinkGirl", 
            "cute/CharacterPrincessGirl", 
            "cute/ChestClosed", 
            "cute/ChestLid", 
            "cute/ChestOpen", 
            "cute/DirtBlock", 
            "cute/DoorTallClosed", 
            "cute/DoorTallOpen", 
            "cute/EnemyBug", 
            "cute/GemBlue", 
            "cute/GemGreen", 
            "cute/GemOrange", 
            "cute/GrassBlock", 
            "cute/Heart", 
            "cute/Key", 
            "cute/PlainBlock", 
            "cute/RampEast", 
            "cute/RampWest", 
            "cute/Rock", 
            "cute/RoofEast", 
            "cute/RoofNorth", 
            "cute/RoofNorthEast", 
            "cute/RoofNorthWest", 
            "cute/RoofSouth", 
            "cute/RoofSouthEast", 
            "cute/RoofSouthWest", 
            "cute/RoofWest", 
            "cute/Selector", 
            "cute/ShadowEast", 
            "cute/ShadowNorth", 
            "cute/ShadowNorthEast", 
            "cute/ShadowNorthWest", 
            "cute/ShadowSideWest", 
            "cute/ShadowSouth", 
            "cute/ShadowSouthEast", 
            "cute/ShadowSouthWest", 
            "cute/ShadowWest", 
            "cute/WoodBlock",
            "cute/Star", 
            "cute/StoneBlock", 
            "cute/StoneBlockTall", 
            "cute/TreeShort", 
            "cute/TreeTall", 
            "space/girl2", 
            "space/girl3", 
            "space/girl4", 
            "space/girl5", 
            "space/healthheart", 
            "space/minus", 
            "space/octopus", 
            "space/planet", 
            "space/plus", 
            "space/rocketship", 
            "space/star", 
            "space/3", 
            "space/4", 
            "space/5", 
            "space/6", 
            "space/7", 
            "space/8", 
            "space/9"
        ];

        window.links = {
            proxyUrl : "https://cors-anywhere.herokuapp.com/",
            image : ["https://www.kasandbox.org/third_party/javascript-khansrc/live-editor/build/images/", 
                     "https://github.com/Khan/live-editor/tree/master/images",
                     "https://www.kasandbox.org/programming-images/"],
        };

        var self = this;

        this.setup = function()
        {
            function code(processing)
            {
                processing.size(400, 400);
                processing.background(255, 255, 255);
                processing.angleMode = "degrees";

                processing.mousePressed = function() {};
                processing.mouseReleased = function() {};
                processing.mouseMoved = function() {};
                processing.mouseDragged = function() {};
                processing.mouseOver = function() {};
                processing.mouseOut = function() {};
                processing.keyPressed = function() {};
                processing.keyReleased = function() {};
                processing.keyTyped = function() {};

                processing.getSound = function(name)
                { 
                    return "Sound"; 
                };
                processing.playSound = function(sound) 
                { 
                    console.log(sound + " is not supported yet..."); 
                };

                processing.getImage = function(name)
                {
                    return (window.cache || self.cache).loadedImages[name] || processing.get(0, 0, 1, 1);
                };

                var lastGet = processing.get;
                processing.get = function()
                {
                    try{
                        return lastGet.apply(this, arguments);
                    }
                    catch(e)
                    {
                        if(arguments[2] !== 0 && arguments[3] !== 0)
                        {
                            console.log(e);
                        }else{
                            throw e;
                        }
                    }
                };

                processing.debug = function(event) 
                {
                    try{
                        return window.console.log.apply(this, arguments);
                    } 
                    catch(e) 
                    {
                        processing.println.apply(this, arguments);
                    }
                };
                processing.Program = {
                    restart: function() 
                    {
                        window.location.reload();
                    },
                    assertEqual: function(equiv) 
                    {
                        if(!equiv) 
                        {
                            console.warn(equiv);
                        }
                    },
                };
            }

            code = combine(new Function("return " + code.toString().split("\n").join(" "))(), any);

            var matched = code.toString().match("this[ ]*\[[ ]*\[[ ]*(\"KAInfiniteLoopSetTimeout\")[ ]*\][ ]*\][ ]*\([ ]*\d*[ ]*\);*");

            if(matched)
            {
                code = new Function("return " + code.toString().replace(matched[0], ""))();
            }

            window.canvas = document.getElementById("canvas"); 
            window.processing = new Processing(canvas, code);
        };

        this.imageProcessing = new Processing(canvas, function(processing)
        {
            try{
                processing.imageCache = JSON.parse(localStorage.getItem("imageCache"));
            }
            catch(e)
            {
                console.log(e);
            }

            if(!processing.imageCache)
            {
                processing.imageCache = {};
            }

            processing.getImage = function(name, callback, url)
            {
                if(name === undefined) { return get(0, 0, 1, 1); }

                url = url || window.links.image[0] + name.split(".")[0] + ".png";
                callback = callback || function() {};

                if(!processing.imageCache)
                {
                    var img = processing.loadImage(url);
                    callback(img, name);
                    return img;
                }
                if(processing.imageCache[name])
                {
                    var img = processing.loadImage(processing.imageCache[name]);
                    callback(img, name);
                    return img;
                }

                toDataURL(window.links.proxyUrl + url, function(dataUrl)
                {
                    processing.imageCache[name] = dataUrl; 
                    localStorage.setItem("imageCache", JSON.stringify(processing.imageCache));
                    callback(processing.imageCache[name], name);
                });

                return processing.loadImage(processing.imageCache[url] || url);
            };

            window.cache.imageNames.forEach(function(element, index, array)
            {
                processing.getImage(element, function(img, name)
                {
                    window.cache.loadedImages[name] = img;

                    if(index === array.length - 1)
                    {
                        (window.setTimeout || function(func)
                        {
                            return func.apply(this, arguments);
                        })
                        (function()
                        {
                            self.setup();
                        }, 50);
                    }
                });
            });
        });
    }

    function combine(a, c)
    {
        var args = Array.prototype.slice.call(arguments);
        var config = {};

        var funcArgs = "";
        var join = "";
        for(var i = 0; i < args.length; i++)
        {
            if(typeof args[i] === "object")
            {
                config = args[i];
                continue;
            }

            var to = args[i].toString();

            var temp = to.substring(to.indexOf('(') + 1, to.indexOf(')'));

            if(temp !== "" && temp !== " ")
            {
                funcArgs += temp + ",";
            }

            join += to.slice(to.indexOf('{') + 1, -1);
        }

        funcArgs = funcArgs.slice(0, -1);

        return new Function("return function any(" + funcArgs + "){" + (config.beginCode || "").replace("\n", "") + join + (config.endCode || "") + "}")();
    }

    function toDataURL(url, callback) 
    {
        var xhr = new XMLHttpRequest();
        xhr.onload = function() 
        {
            var reader = new FileReader();
            reader.onloadend = function() 
            {
                callback(reader.result);
            }
            reader.readAsDataURL(xhr.response);
        };
        xhr.open('GET', url);
        xhr.responseType = 'blob';
        xhr.send();
    }

    return {
        createProcessing: window.createProcessing = this.createProcessing = createProcessing,
        toDataURL: window.toDataURL = this.toDataURL = toDataURL,
        combine: window.combine = this.combine = combine,
    };
}( 
    (window || {}), 
    (JSON || { stringify: function() { return "{}"; }, parse: function() { return {}; } }), 
    (localStorage || { getItem: function() { return {} }, setItem: function() {}, removeItem: function() {} })
));

Index.js:

In your index.js file Usage:

function main()
{
      //Your code here (Trust me it really works!)
}

createProcessing(main);

You can also add more arguments as functions to the createProcessing function. Yeah I have everything except sounds and sometimes (rarely) hyped up controls!

Unfeeling answered 26/3, 2019 at 4:30 Comment(1)
I am looking for this for my kids' projects. This one works and solves the asynchronously loading images issue.Alatea

© 2022 - 2024 — McMap. All rights reserved.