UNPKG

sku-pg

Version:

Skulpt is a Javascript implementation of Python 2.x. Python that runs in your browser!

2,274 lines 72.7 kB
var $builtinmodule = function (name) {
"use strict";

function getConfiguredTarget() {
    var selector, target;

    selector = (Sk.TurtleGraphics && Sk.TurtleGraphics.target) || "turtle",
    target   = typeof selector === "string" ?
        document.getElementById(selector) :
        selector;
    // ensure that the canvas container is empty
    while (target.firstChild) {
        target.removeChild(target.firstChild);
    }
    return target;
}

function generateTurtleModule(_target) {
    var _module              = {},
        _durationSinceRedraw = 0,
        _focus               = true,
        OPTIMAL_FRAME_RATE   = 1000/30,
        SHAPES               = {},
        TURTLE_COUNT         = 0,
        Types                = {},
        _frameRequest,
        _frameRequestTimeout,
        _screenInstance,
        _config,
        _anonymousTurtle,
        _mouseHandler;

    // Ensure that the turtle DOM target has a tabindex
    // so that it can accept keyboard focus and events
    if (!_target.hasAttribute("tabindex")) {
        _target.setAttribute("tabindex", 0);
    }

    Types.FLOAT = function(value) {
        return Sk.builtin.float_(value);
    };
    Types.COLOR = function(value) {
        if (typeof value === "string") {
            return new Sk.builtin.str(value);
        }
        else {
            for(var i = 0; i < 3; i++) {
                value[i] = Sk.builtin.assk$(value[i]);
            }
            if (value.length === 4) {
                value[3] = Sk.builtin.float_(value[3]);
            }
            return new Sk.builtin.tuple(value);
        }
    };
    Types.TURTLE_LIST = function(value) {
        var skValues = [];
        for (var i = 0; i < value.length; i++) {
            skValues.push(value[i].skInstance);
        }
        return new Sk.builtin.tuple(skValues);
    };

    SHAPES.arrow    = [[-10,0],[10,0],[0,10]];
    SHAPES.square   = [[ 10,-10],[10,10],[-10,10],[-10, -10]];
    SHAPES.triangle = [[10,-5.77],[0,11.55],[-10,-5.77]];
    SHAPES.classic  = [[0,0],[-5,-9],[0,-7],[5,-9]];
    SHAPES.turtle   = [
        [0,16],[-2,14],[-1,10],[-4,7],[-7,9],[-9,8],[-6,5],[-7,1],[-5,-3],[-8,-6],
        [-6,-8],[-4,-5],[0,-7],[4,-5],[6,-8],[8,-6],[5,-3],[7,1],[6,5],[9,8],[7,9],
        [4,7],[1,10],[2,14]
    ];

    SHAPES.circle = [
        [10,0],[9.51,3.09],[8.09,5.88],[5.88,8.09],[3.09,9.51],[0,10],[-3.09,9.51],
        [-5.88,8.09],[-8.09,5.88],[-9.51,3.09],[-10,0],[-9.51,-3.09],[-8.09,-5.88],
        [-5.88,-8.09],[-3.09,-9.51],[-0,-10],[3.09,-9.51],[5.88,-8.09],[8.09,-5.88],
        [9.51,-3.09]
    ];

    _config = (function() {
        var defaultSetup = {
                target     : "turtle", // DOM element or id of parent container
                width      : 400, // if set to 0 it will use the target width
                height     : 400, // if set to 0 it will use the target height
                animate    : true, // enabled/disable all animated rendering
                bufferSize : 0, // default turtle buffer size
                allowUndo  : true, // enable ability to use the undo buffer
            },
            key;

        if (!Sk.TurtleGraphics) {
            Sk.TurtleGraphics = {};
        }

        for(key in defaultSetup) {
            if (!Sk.TurtleGraphics.hasOwnProperty(key)) {
                Sk.TurtleGraphics[key] = defaultSetup[key];
            }
        }

        return Sk.TurtleGraphics;
    })();

    // InstantPromise is a workaround to allow usage of the clean promise-style
    // then/catch syntax but to instantly call resolve the then/catch chain so we
    // can avoid creating Suspensions in unnecessary cases.  This is desirable
    // because Suspensions have a fairly large negative impact on overall
    // performance.  These 'instant promises' come into play when a tracer()
    // call is made with a value other than 1.  When tracer is 0 or greater than 1
    // , we can bypass the creation of a Suspension and proceed to the next line of
    // code immediately if the current line is not going to incur involve a screen
    // update. We determine if a real promise or InstantPromise is necessary by
    // checking FrameManager.willRenderNext()
    function InstantPromise() {
        this.lastResult = undefined;
        this.lastError  = undefined;
    }

    InstantPromise.prototype.then = function(cb) {
        if (this.lastError) {
            return this;
        }

        try {
         this.lastResult = cb(this.lastResult);
        } catch(e) {
            this.lastResult = undefined;
            this.lastError  = e;
        }

        return this.lastResult instanceof Promise ? this.lastResult : this;
    };

    InstantPromise.prototype.catch = function(cb) {
        if (this.lastError) {
            try {
                this.lastResult = cb(this.lastError);
                this.lastError  = undefined;
            } catch(e) {
                this.lastResult = undefined;
                this.lastError = e;
            }
        }

        return this.lastResult instanceof Promise ? this.lastResult : this;
    };

    function FrameManager() {
        this.reset();
    }

    var _frameManager;
    function getFrameManager() {
        if (!_frameManager) {
            _frameManager = new FrameManager();
        }
        return _frameManager;
    }

    (function(proto) {
        var browserFrame;
        (function(frame) {
            if (frame) {
                browserFrame = function(method) {
                    return (_frameRequest = frame(method));
                };
            }
        })(window.requestAnimationFrame || window.mozRequestAnimationFrame);

        function animationFrame(delay) {
            if (!_config.animate) {
                return function(method) {
                    method();
                };
            }

            if (!delay && browserFrame) {
                return browserFrame;
            }

            return function(method) {
                _frameRequestTimeout = window.setTimeout(
                    method,
                    delay || OPTIMAL_FRAME_RATE
                );
                 return _frameRequestTimeout;
            };
        }

        proto.willRenderNext = function() {
            return !!(this._buffer && this._frameCount+1 === this.frameBuffer());
        };

        proto.turtles = function() {
            return this._turtles;
        };

        proto.addTurtle = function(turtle) {
            this._turtles.push(turtle);
        };

        proto.reset = function() {
            if (this._turtles) {
                for(var i = this._turtles.length; --i >= 0;) {
                    this._turtles[i].reset();
                }
            }
            this._turtles        = [];
            this._frames         = [];
            this._frameCount     = 0;
            this._buffer         = 1;
            this._rate           = 0;
            this._animationFrame = animationFrame();
        };

        proto.addFrame = function(method, countAsFrame) {
            var instant = false;

            if (countAsFrame) {
                this._frameCount += 1;
            }

            this.frames().push(method);

            instant = (
                !_config.animate ||
                (this._buffer && this._frameCount === this.frameBuffer())
            );

            return instant ? this.update() : new InstantPromise();
        };

        proto.frames = function() {
            return this._frames;
        };

        proto.frameBuffer = function(buffer) {
            if (typeof buffer === "number") {
                this._buffer = buffer | 0;
                if (buffer && buffer <= this._frameCount) {
                    return this.update();
                }
            }
            return this._buffer;
        };

        proto.refreshInterval = function(rate) {
            if (typeof rate === "number") {
                this._rate = rate | 0;
                this._animationFrame = animationFrame(rate);
            }
            return this._rate;
        };

        proto.update = function() {
            return (this._frames && this._frames.length) ?
                this.requestAnimationFrame() :
                new InstantPromise();
        };

        proto.requestAnimationFrame = function() {
            var frames         = this._frames,
                animationFrame = this._animationFrame,
                turtles        = this._turtles,
                sprites        = getScreen().spriteLayer(),
                turtle, i;

            this._frames     = [];
            this._frameCount = 0;

            return new Promise(function(resolve) {
                animationFrame(function paint() {
                    for (i = 0; i < frames.length; i++) {
                        if (frames[i]) {
                            frames[i]();
                        }
                    }
                    clearLayer(sprites);
                    for (i = 0; i < turtles.length; i++) {
                        turtle = turtles[i];
                        if (turtle.getState().shown) {
                            drawTurtle(turtle.getState(), sprites);
                        }
                    }
                    resolve();
                });
            });
        };
    })(FrameManager.prototype);

    function MouseHandler() {
        var self = this;

        this._target   = getTarget();
        this._managers = {};
        this._handlers = {
            mousedown : function(e) {
                self.onEvent("mousedown", e);
            },
            mouseup : function(e) {
                self.onEvent("mouseup", e);
            },
            mousemove : function(e) {
                self.onEvent("mousemove", e);
            }
        };
        for (var key in this._handlers) {
            this._target.addEventListener(key, this._handlers[key]);
        }
    }

    (function(proto) {
        proto.onEvent = function(type, e) {
            var managers     = this._managers[type],
                moveManagers = this._managers["mousemove"],
                computed     = false,
                x, y, localX, localY, i;

            function computeCoordinates() {
                if (computed) return;
                var world = getScreen();
                var rect  = world.spriteLayer().canvas.getBoundingClientRect();
                x         = e.clientX - rect.left | 0;
                y         = e.clientY - rect.top  | 0;
                localX    = x * world.xScale + world.llx;
                localY    = y * world.yScale + world.ury;
                computed = true;
            }

            if ((type === "mousedown" || type === "mouseup") && moveManagers && moveManagers.length) {
                computeCoordinates();
                for (i = moveManagers.length; --i >= 0;) {
                    if (moveManagers[i].test(x, y, localX, localY)) {
                        moveManagers[i].canMove(type === "mousedown");
                    }
                }
            }

            if (managers && managers.length) {
                computeCoordinates();
                for (i = managers.length; --i >= 0;) {
                    if (type === "mousemove") {
                        if (managers[i].canMove()) {
                            managers[i].trigger([localX, localY]);
                        }
                        continue;
                    }

                    if (managers[i].test(x, y, localX, localY)) {
                        managers[i].trigger([localX, localY]);
                    }
                }
            }
        };

        proto.reset = function() {
            this._managers = {};
        };

        proto.addManager = function(type, manager) {
            if (!this._managers[type]) {
                this._managers[type] = [];
            }

            this._managers[type].push(manager);
        };

    })(MouseHandler.prototype);

    function EventManager(type, target) {
        this._type     = type;
        this._target   = target;
        this._handlers = undefined;
        getMouseHandler().addManager(type, this);
    }

    (function(proto) {
        proto.reset = function() {
            this._handlers = undefined;
        };

        proto.canMove = function(value) {
            if (!this._target || !this._target.hitTest) return false;

            if (value !== undefined) {
                this._target.hitTest.hit = value;
            }

            return this._target.hitTest.hit;
        };

        proto.test = function(x, y, localX, localY) {
            return this._target && this._target.hitTest ?
                this._target.hitTest(x, y, localX, localY) :
                !!this._target;
        };

        proto.trigger = function(args) {
            var handlers = this._handlers,
                i;

            if (handlers && handlers.length) {
                for (i = 0; i < handlers.length; i++) {
                    handlers[i].apply({}, args);
                }
            }
        };

        proto.addHandler = function(handler, add) {
            var handlers = this._handlers;

            if (!add && handlers && handlers.length) {
                // remove all existing handlers
                while (handlers.shift()) {/* noop */}
            }

            if (typeof handler !== "function") {
                if (handlers && !handlers.length) {
                    this.reset();
                }
                return;
            }

            if (!handlers) {
                handlers = this._handlers = [];
            }

            handlers.push(handler);
        };
    })(EventManager.prototype);

    function Turtle() {
        getFrameManager().addTurtle(this);
        this._screen = getScreen();
        this._managers = {};
        this.reset();
    }

    Turtle.RADIANS = 2 * Math.PI;

    (function(proto) {
        proto.hitTest = function(mouseX, mouseY, localX, localY) {
            var context = getScreen().hitTestLayer();
            clearLayer(context);
            drawTurtle(this.getState(), context);
            var pixel = context.getImageData(mouseX,mouseY,1,1).data;
            // check alpha first since it is most likely to have a value
            return pixel[3] ||pixel[0] || pixel[1] || pixel[2];
        };

        proto.addUpdate = function(method, countAsFrame, stateChanges) {
            var self  = this,
                state = this.getState(),
                args  = Array.prototype.slice.call(arguments, stateChanges ? 2 : 3);

            return getFrameManager().addFrame(function() {
                if (method) {
                    method.apply(state, args);
                }
                if (stateChanges) {
                    for(var key in stateChanges) {
                        state[key] = stateChanges[key];
                    }
                }
            }, countAsFrame);
        };

        proto.getState = function() {
            var self = this;

            if (!this._state) {
                this._state = {
                    x       : this._x,
                    y       : this._y,
                    angle   : this._angle,
                    radians : this._radians,
                    shape   : this._shape,
                    color   : this._color,
                    fill    : this._fill,
                    filling : this._filling,
                    size    : this._size,
                    speed   : this._computed_speed,
                    down    : this._down,
                    shown   : this._shown,
                    context : function() {
                        return self.getPaper();
                    }
                };
            }
            return this._state;
        };

        proto.translate = function(startX, startY, dx, dy, beginPath, isCircle) {
            var self = this;
            return translate(this, startX, startY, dx, dy, beginPath, isCircle)
                .then(function(coords) {
                    self._x = coords[0];
                    self._y = coords[1];
                });
        };

        proto.rotate = function(startAngle, delta, isCircle) {
            var self = this;
            return rotate(this, startAngle, delta, isCircle)
                .then(function(heading) {
                    self._angle   = heading.angle;
                    self._radians = heading.radians;
                });
        };

        proto.queueMoveBy = function(startX, startY, theta, distance) {
            var dx = Math.cos(theta) * distance,
                dy = Math.sin(theta) * distance;

            return this.translate(startX, startY, dx, dy, true);
        };

        proto.queueTurnTo = function(startAngle, endAngle) {
            endAngle = endAngle % this._fullCircle;
            if (endAngle < 0) {
                endAngle += this._fullCircle;
            }
            return this.rotate(startAngle, endAngle - startAngle);
        };

        proto.getManager = function(type) {
            if (!this._managers[type]) {
                this._managers[type] = new EventManager(type, this);
            }
            return this._managers[type];
        };

        proto.getPaper = function() {
            return this._paper || (this._paper = createLayer(2));
        };

        proto.reset = function() {
            this._x          = 0;
            this._y          = 0;
            this._radians    = 0;
            this._angle      = 0;
            this._shown      = true;
            this._down       = true;
            this._color      = "black";
            this._fill       = "black";
            this._shape      = "classic";
            this._size       = 1;
            this._filling    = false;
            this._undoBuffer = [];
            this._speed      = 3;
            this._computed_speed = 5;
            this._state      = undefined;

            for(var key in this._managers) {
                this._managers[key].reset();
            }

            this._isRadians  = false;
            this._fullCircle = 360;
            this._bufferSize = typeof _config.bufferSize === "number" ?
                _config.bufferSize :
                0;

            removeLayer(this._paper);
            this._paper = undefined;
        };

        proto.$degrees = function(fullCircle) {
            fullCircle = (typeof fullCircle === "number") ?
                Math.abs(fullCircle) :
                360;

            this._isRadians  = false;
            if (!fullCircle || !this._fullCircle) {
                this._angle = this._radians = 0;
            }
            else {
                this._angle = this._angle / this._fullCircle * fullCircle;
            }
            this._fullCircle = fullCircle;
            return this.addUpdate(
                undefined,
                false,
                {angle:this._angle, radians: this._radians}
            );
        };
        proto.$degrees.minArgs     = 0;
        proto.$degrees.keywordArgs = ["fullcircle"];
        proto.$degrees.returnType  = Types.FLOAT;

        proto.$radians = function() {
            if (!this._isRadians) {
                this._isRadians     = true;
                this._angle = this._radians;
                this._fullCircle = Turtle.RADIANS;
            }

            return this._angle;
        };
        proto.$radians.returnType = Types.FLOAT;

        proto.$position = proto.$pos = function() {
            return [this.$xcor(), this.$ycor()];
        };
        proto.$position.returnType = function(value) {
            return new Sk.builtin.tuple([
                    Sk.builtin.float_(value[0]),
                    Sk.builtin.float_(value[1])
            ]);
        };

        proto.$towards = function(x,y) {
            var coords  = getCoordinates(x,y),
                radians = Math.PI + Math.atan2(this._y - coords.y, this._x - coords.x),
                angle   = radians * (this._fullCircle / Turtle.RADIANS);

            return angle;
        };
        proto.$towards.minArgs    = 1;
        proto.$towards.returnType = Types.FLOAT;

        proto.$distance = function(x,y) {
            var coords = getCoordinates(x,y),
                dx     = coords.x - this._x,
                dy     = coords.y - this._y;

            return Math.sqrt(dx * dx + dy * dy);
        };
        proto.$distance.minArgs    = 1;
        proto.$distance.returnType = Types.FLOAT;

        proto.$heading = function() {
            return Math.abs(this._angle) < 1e-13 ? 0 : this._angle;
        };
        proto.$heading.returnType = Types.FLOAT;

        proto.$xcor = function() {
            return Math.abs(this._x) < 1e-13 ? 0 : this._x;
        };
        proto.$xcor.returnType = Types.FLOAT;

        proto.$ycor = function() {
            return Math.abs(this._y) < 1e-13 ? 0 : this._y;
        };
        proto.$ycor.returnType = Types.FLOAT;

        proto.$forward = proto.$fd = function(distance) {
            pushUndo(this);
            return this.queueMoveBy(this._x, this._y, this._radians, distance);
        };

        proto.$undo = function() {
            popUndo(this);
        };

        proto.$undobufferentries = function() {
            return this._undoBuffer.length;
        };

        proto.$setundobuffer = function(size) {
            this._bufferSize = typeof size === "number" ?
                Math.min(Math.abs(size), 1000) :
                0;
        };

        proto.$backward = proto.$back = proto.$bk = function(distance) {
            pushUndo(this);
            return this.queueMoveBy(this._x, this._y, this._radians, -distance);
        };

        proto.$goto_$rw$ = proto.$setpos = proto.$setposition = function(x,y) {
            var coords = getCoordinates(x,y);

            pushUndo(this);

            return this.translate(
                this._x, this._y,
                coords.x - this._x, coords.y - this._y,
                true
            );
        };
        proto.$goto_$rw$.minArgs = 1;

        proto.$setx = function(x) {
            return this.translate(this._x, this._y, x - this._x, 0, true);
        };

        proto.$sety = function(y) {
            return this.translate(this._x, this._y, 0, y - this._y, true);
        };

        proto.$home = function() {
            var self  = this,
                angle = this._angle;

            pushUndo(this);
            return self.translate(this._x, this._y, -this._x, -this._y, true)
                .then(function(position) {
                    return self.queueTurnTo(angle, 0);
                })
                .then(function(heading) {
                    return undefined;
                });
        };

        proto.$right = proto.$rt = function(angle) {
            pushUndo(this);
            return this.rotate(this._angle, -angle);
        };

        proto.$left = proto.$lt = function(angle) {
            pushUndo(this);
            return this.rotate(this._angle, angle);
        };

        proto.$setheading = proto.$seth = function(angle) {
            pushUndo(this);
            return this.queueTurnTo(this._angle, angle);
        };

        function circleRotate(turtle, angle, radians) {
            return function() {
                return turtle.addUpdate(
                    undefined,
                    false,{angle:angle, radians:radians}
                );
            };
        }

        function circleSegment(turtle, x, y, dx, dy, beginPath) {
            return function() {
                return turtle.translate(x, y, dx, dy, beginPath, true);
            };
        }

        proto.$circle = function(radius, extent, steps) {
            var self      = this,
                x         = this._x,
                y         = this._y,
                angle     = this._angle,
                heading   = {},
                states    = [],
                scale     = 1/getScreen().lineScale,
                beginPath = true,
                endAngle, frac, w, w2, l, i, dx, dy, promise;

            pushUndo(this);

            if (extent === undefined) {
                extent = self._fullCircle;
            }

            if (steps === undefined) {
                frac  = Math.abs(extent)/self._fullCircle;
                steps = 1 + ((Math.min(11+Math.abs(radius*scale)/6, 59)*frac) | 0);
            }
            w  = extent / steps;
            w2 = 0.5 * w;
            l  = 2 * radius * Math.sin(w*Math.PI/self._fullCircle);

            if (radius < 0) {
                l = -l;
                w = -w;
                w2 = -w2;
                endAngle = angle - extent;
            }
            else {
                endAngle = angle + extent;
            }

            promise = getFrameManager().willRenderNext() ? Promise.resolve() : new InstantPromise();

            angle += w2;

            for(i = 0; i < steps; i++) {
                calculateHeading(self, angle + w * i, heading);
                dx = Math.cos(heading.radians) * l;
                dy = Math.sin(heading.radians) * l;
                promise = promise
                    .then(circleRotate(self, heading.angle, heading.radians))
                    .then(circleSegment(self, x, y, dx, dy, beginPath));
                x += dx;
                y += dy;
                beginPath = false;
            }

            promise = promise.then(function() {
                calculateHeading(self, endAngle, heading);
                self._angle   = heading.angle;
                self._radians = heading.radians;
                return self.addUpdate(undefined, true, heading);
            });

            return promise;
        };
        proto.$circle.keywordArgs = ["extent", "steps"];
        proto.$circle.minArgs     = 1;

        proto.$penup = proto.$up = proto.$pu = function() {
            this._down = false;
            return this.addUpdate(undefined, false, {down:false});
        };

        proto.$pendown = proto.$down = proto.$pd = function() {
            this._down = true;
            return this.addUpdate(undefined, false, {down:true});
        };

        proto.$isdown = function() {
            return this._down;
        };

        proto.$speed = function(speed) {
            if (arguments.length) {
                this._speed          = Math.max(0, Math.min(1000, speed));
                this._computed_speed = Math.max(0, speed * 2 - 1);
                return this.addUpdate(undefined, false, {speed:this._computed_speed});
            }

            return this._speed;
        };
        proto.$speed.minArgs = 0;
        proto.$speed.keywordArgs = ["speed"];

        proto.$pencolor = function(r,g,b,a) {
            var color;

            if (arguments.length) {
                this._color = createColor(r,g,b,a);
                return this.addUpdate(undefined, this._shown, {color : this._color});
            }

            return hexToRGB(this._color);
        };
        proto.$pencolor.minArgs = 0;
        proto.$pencolor.returnType = Types.COLOR;

        proto.$fillcolor = function(r,g,b,a) {
            var color;

            if (arguments.length) {
                this._fill = createColor(r,g,b,a);
                return this.addUpdate(undefined, this._shown, {fill : this._fill});
            }

            return hexToRGB(this._fill);
        };
        proto.$fillcolor.minArgs = 0;
        proto.$fillcolor.returnType = Types.COLOR;

        proto.$color = function(color, fill, b, a) {
            if (arguments.length) {
                if (arguments.length === 1 || arguments.length >= 3) {
                    this._color = createColor(color, fill, b, a);
                    this._fill  = this._color;
                }
                else {
                    this._color = createColor(color);
                    this._fill  = createColor(fill);
                }
                return this.addUpdate(undefined, this._shown, {
                    color : this._color,
                    fill  : this._fill
                });
            }
            return [this.$pencolor(), this.$fillcolor()];
        };
        proto.$color.minArgs = 0;
        proto.$color.returnType = function(value) {
            return new Sk.builtin.tuple([
                Types.COLOR(value[0]),
                Types.COLOR(value[1])
            ]);
        };

        proto.$fill = function(flag) {
            var self = this;

            if (flag !== undefined) {
                flag = !!flag;
                if (flag === this._filling) return;
                this._filling = flag;
                if (flag) {
                    pushUndo(this);
                    return this.addUpdate(undefined, false, {
                        filling      : true,
                        fillBuffer : [{x : this._x, y : this._y}]
                    });
                }
                else {
                    pushUndo(this);
                    return this.addUpdate(
                        function() {
                            this.fillBuffer.push(this);
                            drawFill.call(this);
                        },
                        true,
                        {
                            filling      : false,
                            fillBuffer : undefined
                        }
                    );
                }
            }

            return this._filling;
        };
        proto.$fill.minArgs = 0;

        proto.$begin_fill = function() {
            return this.$fill(true);
        };

        proto.$end_fill = function() {
            return this.$fill(false);
        };

        proto.$stamp = function() {
            pushUndo(this);
            return this.addUpdate(function() {
                drawTurtle(this, this.context());
            }, true);
        };

        proto.$dot = function(size, color, g, b, a) {
            pushUndo(this);
            size = Sk.builtin.asnum$(size);
            size = (typeof size === "number") ?
                Math.max(1, Math.abs(size) | 0) :
                Math.max(this._size + 4, this._size * 2);

            color = (color !== undefined) ?
                createColor(color, g, b, a) :
                this._color;

            return this.addUpdate(drawDot, true, undefined, size, color);
        };

        proto.$write = function(message,move,align,font) {
            var self = this,
                promise, face, size, type, width;

            pushUndo(this);

            message = String(message);

            if (font && font.constructor === Array) {
                face = typeof font[0] === "string" ? font[0] : "Arial";
                size = String(font[1] || "12pt");
                type = typeof font[2] === "string" ? font[2] : "normal";
                if (/^\d+$/.test(size)) {
                    size += "pt";
                }

                font = [type, size, face].join(" ");
            }

            if (!align) {
                align = "left";
            }

            promise = this.addUpdate(
                drawText, true, undefined, message, align, font
            );

            if (move && (align === "left" || align === "center")) {
                width = measureText(message, font);
                if (align === "center") {
                    width = width/2;
                }
                promise = promise.then(function() {
                    var state = self.getState();
                    return self.translate(state.x, state.y, width, 0, true);
                });
            }

            return promise;
        };
        proto.$write.keywordArgs = ["move","align","font"];
        proto.$write.minArgs     = 1;

        proto.$pensize = proto.$width = function(size) {
            if (arguments.length) {
                this._size = size;
                return this.addUpdate(undefined, this._shown, {size : size});
            }

            return this._size;
        };
        proto.$pensize.minArgs = proto.$width.minArgs = 0;
        proto.$pensize.keywordArgs = proto.$width.keywordArgs = ["width"];

        proto.$showturtle = proto.$st = function() {
            this._shown = true;
            return this.addUpdate(undefined, true, {shown : true});
        };

        proto.$hideturtle = proto.$ht = function() {
            this._shown = false;
            return this.addUpdate(undefined, true, {shown : false});
        };

        proto.$isvisible = function() {
            return this._shown;
        };

        proto.$shape = function(shape) {
            if (shape && SHAPES[shape]) {
                this._shape = shape;
                return this.addUpdate(undefined, this._shown, {shape : shape});
            }

            return this._shape;
        };
        proto.$shape.minArgs     = 0;
        proto.$shape.keywordArgs = ["name"];

        proto.$window_width = function() {
            return this._screen.$window_width();
        };
        
        proto.$window_height = function() {
            return this._screen.$window_height();
        };
        
        proto.$tracer = function(n, delay) {
            return this._screen.$tracer(n, delay);
        };
        proto.$tracer.minArgs     = 0;
        proto.$tracer.keywordArgs = ["n", "delay"];
        
        proto.$update = function() {
            return this._screen.$update();
        };
        
        proto.$delay = function(delay) {
            return this._screen.$delay(delay);
        };
        proto.$delay.minArgs     = 0;
        proto.$delay.keywordArgs = ["delay"];

        proto.$reset = function() {
            this.reset();
            return this.$clear();
        };

        proto.$mainloop = proto.$done = function() {
            return this._screen.$mainloop();
        };

        proto.$clear = function() {
            return this.addUpdate(function() {
                clearLayer(this.context());
            }, true);
        };
        proto.$dot.minArgs = 0;

        proto.$onclick = function(method,btn,add) {
            this.getManager("mousedown").addHandler(method, add);
        };
        proto.$onclick.minArgs = 1;
        proto.$onclick.keywordArgs = ["btn","add"];

        proto.$onrelease = function(method,btn,add) {
            this.getManager("mouseup").addHandler(method, add);
        };
        proto.$onrelease.minArgs = 1;
        proto.$onrelease.keywordArgs = ["btn","add"];

        proto.$ondrag = function(method,btn,add) {
            this.getManager("mousemove").addHandler(method, add);
        };
        proto.$ondrag.minArgs = 1;
        proto.$ondrag.keywordArgs = ["btn","add"];

        proto.$getscreen = function() {
            return _module.Screen();
        };
        proto.$getscreen.isSk = true;

        proto.$clone = function() {

            var newTurtleInstance = Sk.misceval.callsimOrSuspend(_module.Turtle);

            // All the properties that are in getState()
            newTurtleInstance.instance._x = this._x;
            newTurtleInstance.instance._y = this._y;
            newTurtleInstance.instance._angle = this._angle;
            newTurtleInstance.instance._radians = this._radians;
            newTurtleInstance.instance._shape = this._shape;
            newTurtleInstance.instance._color = this._color;
            newTurtleInstance.instance._fill = this._fill;
            newTurtleInstance.instance._filling = this._filling;
            newTurtleInstance.instance._size = this._size;
            newTurtleInstance.instance._computed_speed = this._computed_speed;
            newTurtleInstance.instance._down = this._down;
            newTurtleInstance.instance._shown = this._shown;

            // Other properties to copy
            newTurtleInstance.instance._isRadians = this._isRadians;
            newTurtleInstance.instance._fullCircle = this._fullCircle;
            newTurtleInstance.instance._bufferSize = this._bufferSize;
            newTurtleInstance.instance._undoBuffer = this._undoBuffer;


            newTurtleInstance._clonedFrom = this;

            return newTurtleInstance;
        };
        proto.$clone.returnType = function(value) {
            // When I return the instance here, I'm not sure if it ends up with the right "Turtle" python type.
            return value
        };

        proto.$getturtle = proto.$getpen = function() {
            return this.skInstance;
        };
        proto.$getturtle.isSk = true;
    })(Turtle.prototype);

    function Screen() {
        var w,h;
        this._frames    = 1;
        this._delay     = undefined;
        this._bgcolor   = "none";
        this._mode      = "standard";
        this._managers  = {};
        this._keyLogger = {};
        if (_config.height && _config.width) {
            w = _config.width/2;
            h = _config.height/2;
        } else {
            w = _config.defaultSetup.width/2;
            h = _config.defaultSetup.height/2;
        }
        this.setUpWorld(-w,-h,w,h);
    }

    (function(proto) {
        proto.spriteLayer = function() {
            return this._sprites || (this._sprites = createLayer(3));
        };

        proto.bgLayer = function() {
            return this._background || (this._background = createLayer(1));
        };

        proto.hitTestLayer = function() {
            return this._hitTest || (this._hitTest = createLayer(0,true));
        };

        proto.getManager = function(type) {
            if (!this._managers[type]) {
                this._managers[type] = new EventManager(type, this);
            }
            return this._managers[type];
        };

        proto.reset = function() {
            var key;

            this._keyListeners = undefined;

            for (key in this._keyLogger) {
                window.clearInterval(this._keyLogger[key]);
                window.clearTimeout(this._keyLogger[key]);
                delete this._keyLogger[key];
            }

            if (this._keyDownListener) {
                getTarget().removeEventListener("keydown", this._keyDownListener);
                this._keyDownListener = undefined;
            }

            if (this._keyUpListener) {
                getTarget().removeEventListener("keyup", this._keyUpListener);
                this._keyUpListener = undefined;
            }

            if (this._timer) {
                window.clearTimeout(this._timer);
                this._timer = undefined;
            }

            for(key in this._managers) {
                this._managers[key].reset();
            }

            this._mode = "standard";
            removeLayer(this._sprites);
            this._sprites = undefined;
            removeLayer(this._background);
            this._background = undefined;
        };

        proto.setUpWorld = function(llx, lly, urx, ury) {
            var world = this;

            world.llx       = llx;
            world.lly       = lly;
            world.urx       = urx;
            world.ury       = ury;
            world.xScale    = (urx - llx) / getWidth();
            world.yScale    = -1 * (ury - lly) / getHeight();
            world.lineScale = Math.min(Math.abs(world.xScale), Math.abs(world.yScale));
        };

        proto.$setup = function(width, height, startX, startY) {
            if (isNaN(parseFloat(width))) {
                width = getWidth();
            }
            if (isNaN(parseFloat(height))) {
                height = getHeight();
            }

            if (width <= 1) {
                width = getWidth() * width;
            }
            if (height <= 1) {
                height = getHeight() * height;
            }

            this._width  = width;
            this._height = height;

            this._xOffset = (startX !== undefined && !isNaN(parseInt(startX))) ?
                parseInt(startX) :
                0;

            this._yOffset = (startY !== undefined && !isNaN(parseInt(startY))) ?
                parseInt(startY) :
                0;

            if (this._mode === "world") {
                return this._setworldcoordinates(this.llx, this.lly, this.urx, this.ury);
            }

            return this._setworldcoordinates(-width/2, -height/2, width/2, height/2);
        };
        proto.$setup.minArgs     = 0;
        proto.$setup.keywordArgs = ["width", "height", "startx", "starty"];

        proto.$register_shape = proto.$addshape = function(name, points) {
            SHAPES[name] = points;
        };

        proto.$getshapes = function() {
            return Object.keys(SHAPES);
        };

        proto.$tracer = function(frames, delay) {
            if (frames !== undefined || delay !== undefined) {
                if (typeof delay === "number") {
                    this._delay = delay;
                    getFrameManager().refreshInterval(delay);
                }
                if (typeof frames === "number") {
                    this._frames = frames;
                    return getFrameManager().frameBuffer(frames);
                }

                return;
            }

            return this._frames;
        };
        proto.$tracer.minArgs = 0;

        proto.$delay = function(delay) {
            if (delay !== undefined) {
                return this.$tracer(undefined, delay);
            }

            return this._delay === undefined ? OPTIMAL_FRAME_RATE : this._delay;
        };

        proto._setworldcoordinates = function(llx, lly, urx, ury) {
            var world     = this,
                turtles = getFrameManager().turtles();

            this.setUpWorld(llx, lly, urx, ury);

            if (this._sprites) {
                applyWorld(this, this._sprites);
            }

            if (this._background) {
                applyWorld(this, this._background);
            }

            return this.$clear();
        };

        proto.$setworldcoordinates = function(llx, lly, urx, ury) {
            this._mode = "world";
            return this._setworldcoordinates(llx, lly, urx, ury);
        };

        proto.$clear = proto.$clearscreen = function() {
            this.reset();
            return this.$reset();
        };

        proto.$update = function() {
            return getFrameManager().update();
        };

        proto.$reset = proto.$resetscreen = function() {
            var self = this,
                turtles = getFrameManager().turtles();

            return getFrameManager().addFrame(function() {
                applyWorld(self, self._sprites);
                applyWorld(self, self._background);
                for(var i = 0; i < turtles.length; i++) {
                    turtles[i].reset();
                    applyWorld(self, turtles[i]._paper);
                }
            }, true);
        };

        proto.$window_width = function() {
            return getWidth();
        };

        proto.$window_height = function() {
            return getHeight();
        };
        proto.$delay.minArgs = 0;

        proto.$turtles = function() {
            return getFrameManager().turtles();
        };
        proto.$turtles.returnType = Types.TURTLE_LIST;

        proto.$bgcolor = function(color, g, b, a) {
            if (arguments.length) {
                this._bgcolor = createColor(color, g, b, a);
                clearLayer(this.bgLayer(), this._bgcolor);
                return;
            }

            return hexToRGB(this._bgcolor);
        };
        proto.$bgcolor.minArgs = 0;
        proto.$bgcolor.returnType = Types.COLOR;

        // no-op - just defined for consistency with python version
        proto.$mainloop = proto.$done = function() {
            return undefined;
        };

        proto.$bye = function() {
            return Sk.TurtleGraphics.reset();
        };

        proto.$exitonclick = function() {
            this._exitOnClick = true;
            return this.getManager("mousedown").addHandler(function() {
                resetTurtle();
            }, false);
        };

        proto.$onclick = function(method,btn,add) {
            if (this._exitOnClick) return;
            this.getManager("mousedown").addHandler(method, add);
        };
        proto.$onclick.minArgs = 1;
        proto.$onclick.keywordArgs = ["btn","add"];

        var KEY_MAP = {
            "8"  : /^back(space)?$/i,
            "9"  : /^tab$/i,
            "13" : /^(enter|return)$/i,
            "16" : /^shift$/i,
            "17" : /^(ctrl|control)$/i,
            "18" : /^alt$/i,
            "27" : /^esc(ape)?$/i,
            "32" : /^space$/i,
            "33" : /^page[\s\-]?up$/i,
            "34" : /^page[\s\-]?down$/i,
            "35" : /^end$/i,
            "36" : /^home$/i,
            "37" : /^left([\s\-]?arrow)?$/i,
            "38" : /^up([\s\-]?arrow)?$/i,
            "39" : /^right([\s\-]?arrow)?$/i,
            "40" : /^down([\s\-]?arrow)?$/i,
            "45" : /^insert$/i,
            "46" : /^del(ete)?$/i
        };

        proto._createKeyRepeater = function(key, code) {
            var self = this;
            // set a timeout for 333ms and if key has not yet been
            // released, fire another event and continue firing
            // at a rate of ~20 times per second until key is released
            self._keyLogger[code] = window.setTimeout(function() {
                // trigger the first repeat after the longer delay
                self._keyListeners[key]();
                // set up the repeat interval with the quick delay
                self._keyLogger[code] = window.setInterval(function() {
                    self._keyListeners[key]();
                }, 50);
            }, 333);
        };

        proto._createKeyDownListener = function() {
            var self = this;

            if (this._keyDownListener) return;

            this._keyDownListener = function(e) {
                if (!focusTurtle()) return;

                var code    = e.charCode || e.keyCode,
                    pressed = String.fromCharCode(code).toLowerCase(),
                    key, inKeyMap;

                if (self._keyLogger[code]) return;

                for (key in self._keyListeners) {
                    inKeyMap = (key.length > 1 && KEY_MAP[code] && KEY_MAP[code].test(key));
                    if (key === pressed || inKeyMap) {
                        // trigger the intial keydown handler
                        self._keyListeners[key]();
                        self._createKeyRepeater(key, code);
                        e.preventDefault();
                        break;
                    }
                }
            };

            getTarget().addEventListener("keydown", this._keyDownListener);
        };

        proto._createKeyUpListener = function() {
            var self = this;

            if (this._keyUpListener) return;

            this._keyUpListener = function(e) {
                var interval = self._keyLogger[e.charCode || e.keyCode];
                if (interval !== undefined) {
                    e.preventDefault();
                    window.clearInterval(interval);
                    window.clearTimeout(interval);
                    delete(self._keyLogger[e.charCode || e.keyCode]);
                }
            };
            
            getTarget().addEventListener("keyup", this._keyUpListener);
        };

        proto.$listen = function() {
            this._createKeyUpListener();
            this._createKeyDownListener();
        };

        proto.$onkey = function(method, keyValue) {
            if (typeof keyValue === "function") {
                var temp = method;
                method   = keyValue;
                keyValue = temp;
            }

            keyValue = String(keyValue).toLowerCase();

            if (method && typeof method === "function") {
                if (!this._keyListeners) this._keyListeners = {};
                this._keyListeners[keyValue] = method;
            }
            else {
                delete this._keyListeners[keyValue];
            }
        };

        proto.$onscreenclick = function(method,btn,add) {
            this.getManager("mousedown").addHandler(method, add);
        };
        proto.$onscreenclick.minArgs = 1;
        proto.$onscreenclick.keywordArgs = ["btn","add"];

        proto.$ontimer = function(method, interval) {
            if (this._timer) {
                window.clearTimeout(this._timer);
                this._timer = undefined;
            }

            if (method && typeof interval === "number") {
                this._timer = window.setTimeout(method, Math.max(0, interval|0));
            }
        };
        proto.$ontimer.minArgs = 0;

    })(Screen.prototype);

    function ensureAnonymous() {
        if (!_anonymousTurtle) {
            _anonymousTurtle = _module.Turtle();
        }

        return _anonymousTurtle.instance;
    }

    function getTarget() {
        return _target;
    }

    function getScreen() {
        if (!_screenInstance) {
            _screenInstance = new Screen();
        }
        return _screenInstance;
    }

    function getMouseHandler() {
        if (!_mouseHandler) {
            _mouseHandler = new MouseHandler();
        }
        return _mouseHandler;
    }

    function getWidth() {
        return (
            (_screenInstance && _screenInstance._width) ||
            _config.width ||
            getTarget().clientWidth
        ) | 0;
    }

    function getHeight() {
        return (
            (_screenInstance && _screenInstance._height) ||
            _config.height ||
            getTarget().clientHeight
        ) | 0;
    }

    function createLayer(zIndex, isHidden) {
        var canvas = document.createElement("canvas"),
            width  = getWidth(),
            height = getHeight(),
            offset = getTarget().firstChild ? (-height) + "px" : "0",
            context;

        canvas.width          = width;
        canvas.height         = height;
        canvas.style.position = "relative";
        canvas.style.display  = "block";
        canvas.style.setProperty("margin-top",offset);
        canvas.style.setProperty("z-index", zIndex);
        if (isHidden) {
            canvas.style.display = "none";
        }

        getTarget().appendChild(canvas);

        context = canvas.getContext("2d");
        context.lineCap = "round";
        context.lineJoin = "round";

        applyWorld(getScreen(), context);

        return context;
    }

    function cancelAnimationFrame() {
        if (_frameRequest) {
            (window.cancelAnimationFrame || window.mozCancelAnimationFrame)(_frameRequest);
            _frameRequest = undefined;
        }
        if (_frameRequestTimeout) {
            window.clearTimeout(_frameRequestTimeout);
            _frameRequestTimeout = undefined;
        }
    }

    function applyWorld(world, context) {
        var llx    = world.llx,
            lly    = world.lly,
            urx    = world.urx,
            ury    = world.ury,
            xScale = world.xScale,
            yScale = world.yScale;

        if (!context) return;

        clearLayer(context);

        context.restore();
        context.save();
        context.scale(1 / xScale, 1 / yScale);
        if (lly === 0) {
            context.translate(-llx, lly - (ury - lly));
        } else if (lly > 0) {
            context.translate(-llx, -lly * 2);
        } else {
            context.translate(-llx, -ury);
        }
    }

    function pushUndo(turtle) {
        var properties, undoState, i;

        if (!_config.allowUndo || !turtle._bufferSize) {
            return;
        }

        if (!turtle._undoBuffer) {
            turtle._undoBuffer = [];
        }

        while(turtle._undoBuffer.length > turtle._bufferSize) {
            turtle._undoBuffer.shift();
        }

        undoState  = {};
        properties = "x y angle radians color fill down filling shown shape size".split(" ");
        for(i = 0; i < properties.length; i++) {
            undoState[properties[i]] = turtle["_" + properties[i]];
        }

        turtle._undoBuffer.push(undoState);

        return turtle.addUpdate(function() {
            undoState.fillBuffer = this.fillBuffer ? this.fillBuffer.slice() : undefined;
            if (turtle._paper && turtle._paper.canvas) {
                undoState.image = turtle._paper.canvas.toDataURL();
            }
        }, false);
    }

    var undoImage = new Image();
    function popUndo(turtle) {
        var undoState;

        if (!turtle._bufferSize || !turtle._undoBuffer) {
            return;
        }

        undoState = turtle._undoBuffer.pop();

        if (!undoState) {
            return;
        }

        for(var key in undoState) {
            if (key === "image" || key === "fillBuffer") continue;
            turtle["_" + key] = undoState[key];
        }

        return turtle.addUpdate(function() {
            var img;
            if (undoState.image) {
                undoImage.src = undoState.image;
                img = undoImage;
            }

            clearLayer(this.context(), false, undoImage);
            delete undoState.image;
        }, true, undoState);
    }

    function removeLayer(layer) {
        if (layer && layer.canvas && layer.canvas.parentNode) {
            layer.canvas.parentNode.removeChild(layer.canvas);
        }
    }

    function clearLayer(context, color, image) {
        if (!context) return;

        context.save();
        context.setTransform(1,0,0,1,0,0);
        if (color) {
            context.fillStyle = color;
            context.fillRect(0, 0, context.canvas.width, context.canvas.height);
        }
        else {
            context.clearRect(0, 0, context.canvas.width, context.canvas.height);
        }

        if (image) {
            context.drawImage(image, 0, 0);
        }

        context.restore();
    }

    function drawTurtle(state, context) {
        var shape  = SHAPES[state.shape],
            world  = getScreen(),
            width  = getWidth(),
            height = getHeight(),
            xScale = world.xScale,
            yScale = world.yScale,
            x, y, bearing;

        if (!context) return;

        x       = Math.cos(state.radians) / xScale;
        y       = Math.sin(state.radians) / yScale;
        bearing = Math.atan2(y, x) - Math.PI/2;

        context.save();
        context.translate(state.x, state.y);
        context.scale(xScale,yScale);
        context.rotate(bearing);
        context.beginPath();
        context.lineWidth   = 1;
        context.strokeStyle = state.color;
        context.fillStyle   = state.fill;
        context.moveTo(shape[0][0], shape[0][1]);
        for(var i = 1; i < shape.length; i++) {
            context.lineTo(shape[i][0], shape[i][1]);
        }
        context.closePath();
        context.fill();
        context.stroke();
        context.restore();
    }

    function drawDot(size, color) {
        var context = this.context(),
            screen  = getScreen(),
            xScale  = screen.xScale,
            yScale  = screen.yScale;

        if (!context) return;
        context.beginPath();
        context.moveTo(this.x, this.y);
        size = size * Math.min(Math.abs(xScale),Math.abs(yScale));
        context.arc(this.x, this.y, size, 0, Turtle.RADIANS);
        context.closePath();
        context.fillStyle = color || this.color;
        context.fill();
    }

    var textMeasuringContext = document.createElement("canvas").getContext("2d");
    function measureText(message, font) {
        if (font) {
            textMeasuringContext.font = font;
        }
        return textMeasuringContext.measureText(message).width;
    }

    function drawText(message, align, font) {
        var context = this.context();

        if (!context) return;

        context.save();
        if (font) {
            context.font = font;
        }
        if (align && align.match(/^(left|right|center)$/)) {
            context.textAlign = align;
        }

        context.scale(1,-1);
        context.fillStyle = this.fill;
        context.fillText(message, this.x, -this.y);
        context.restore();
    }

    function drawLine(loc, beginPath, endPath) {
        // TODO: make steps in path use square ends of lines
        // and open and close path at the right times.
        // See if we can minimize calls to stroke
        var context = this.context();

        if (!context) return;

        if (beginPath) {
            context.beginPath();
            context.moveTo(this.x, this.y);
        }

        context.lineWidth   = this.size * getScreen().lineScale;
        context.strokeStyle = this.color;
        context.lineTo(loc.x, loc.y);
        context.stroke();
    }

    function drawFill() {
        var context = this.context(),
            path  = this.fillBuffer,
            i;

        if (!context || !path || !path.length) return;

        context.save();
        context.beginPath();
        context.moveTo(path[0].x,path[0].y);
        for(i = 1; i < path.length; i++) {
            context.lineTo(path[i].x, path[i].y);
        }
        context.closePath();
        context.fillStyle = this.fill;
        context.fill();
        for(i = 1; i < path.length; i++) {
            if (!path[i].stroke) {
                continue;
            }

            context.beginPath();
            context.moveTo(path[i-1].x, path[i-1].y);
            context.lineWidth   = path[i].size * getScreen().lineScale;
            context.strokeStyle = path[i].color;
            context.lineTo(path[i].x, path[i].y);
            context.stroke();
        }
        context.restore();
    }

    function partialTranslate(turtle, x, y, beginPath, countAsFrame) {
        return function() {
            return turtle.addUpdate(
                function(loc) {
                    if (this.down) {
                        drawLine.call(this, loc, beginPath);
                    }
                },
                countAsFrame,
                {x : x, y : y},
                beginPath
            );
        };
    }

    function translate(turtle, startX, startY, dx, dy, beginPath, isCircle) {
        // speed is in pixels per ms
        var speed   = turtle._computed_speed,
            screen  = getScreen(),
            xScale  = Math.abs(screen.xScale),
            yScale  = Math.abs(screen.yScale),
            x       = startX,
            y       = startY,
            pixels  = Math.sqrt(dx * dx * xScale + dy * dy * yScale),
            // TODO: allow fractional frame updates?
            frames  = speed ? Math.round(Math.max(1, pixels / speed)) : 1,
            xStep   = dx / frames,
            yStep   = dy / frames,
            promise = getFrameManager().willRenderNext() ?
                Promise.resolve() :
                new InstantPromise(),
            countAsFrame = (!speed && isCircle) ? false : true,
            i;

        turtle.addUpdate(function() {
            if (this.filling) {
                this.fillBuffer.push({
                    x        : this.x,
                    y      : this.y,
                    stroke : this.down,
                    color  : this.color,
                    size   : this.size
                });
            }
        }, false);

        for(i = 0; i < frames; i++) {
            x = startX + xStep * (i+1);
            y = startY + yStep * (i+1);
            promise = promise.then(
                partialTranslate(turtle, x, y, beginPath, countAsFrame)
            );
            beginPath = false;
        }

        return promise.then(function() {
            return [startX + dx, startY + dy];
        });
    }

    function partialRotate(turtle, angle, radians, countAsFrame) {
        return function() {
            return turtle.addUpdate(undefined, countAsFrame, {angle:angle, radians:radians});
        };
    }

    function rotate(turtle, startAngle, delta, isCircle) {
        var speed        = turtle._computed_speed,
            degrees    = delta / turtle._fullCircle * 360,
            frames     = speed ? Math.round(Math.max(1, Math.abs(degrees) / speed)) : 1,
            dAngle     = delta / frames,
            heading    = {},
            countAsFrame = (!speed && isCircle) ? false : true,
            promise    = getFrameManager().willRenderNext() ?
                Promise.resolve() :
                new InstantPromise(),
            i;

        // TODO: request how many frames are remaining and only queue up
        // a single rotation per screen update

        for(i = 0; i < frames; i++) {
            calculateHeading(turtle, startAngle + dAngle * (i+1), heading);
            promise = promise.then(
                partialRotate(turtle, heading.angle, heading.radians, countAsFrame)
            );
        }

        return promise.then(function() {
            return calculateHeading(turtle, startAngle + delta);
        });
    }

    function getCoordinates(x, y) {
        if (y === undefined) {
            y = (x && (x.y || x._y || x[1])) || 0;
            x = (x && (x.x || x._x || x[0])) || 0;
        }
        return {x:x, y:y};
    }

    // Modified solution of Tim Down's version from stackoverflow
    // http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
    function hexToRGB(hex) {
        var rgbForm, hexForm, result;

        if (rgbForm = /^rgba?\((\d+),(\d+),(\d+)(?:,([.\d]+))?\)$/.exec(hex)) {
            result = [
                parseInt(rgbForm[1]),
                parseInt(rgbForm[2]),
                parseInt(rgbForm[3])
            ];
            if (rgbForm[4]) {
                result.push(parseFloat(rgbForm[4]));
            }
        }
        else if (/^#?[a-f\d]{3}|[a-f\d]{6}$/i.exec(hex)) {
            if (hex.length === 4) {
                // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
                hex = hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, function(m, r, g, b) {
                        return r + r + g + g + b + b;
                });
            }

            hexForm = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
            result = [
                parseInt(hexForm[1], 16),
                parseInt(hexForm[2], 16),
                parseInt(hexForm[3], 16)
            ];
        }
        else {
            result = hex;
        }

        return result;
    }

    function createColor(color, g, b, a) {
        var i;

        if (g !== undefined) {
            color = [color, g, b, a];
        }

        if (color.constructor === Array && color.length) {
            for(i = 0; i < 3; i++) {
                color[i] = (typeof color[i] === "number") ?
                    Math.max(0, Math.min(255, parseInt(color[i]))) :
                    0;
            }
            if (typeof color[i] === "number") {
                color[3] = Math.max(0, Math.min(1, color[i]));
                color = "rgba(" + color.join(",") + ")";
            }
            else {
                color = "rgb(" + color.slice(0,3).join(",") + ")";
            }
        }
        else if (typeof color === "string" && !color.match(/\s*url\s*\(/i)) {
            color = color.replace(/\s+/g, "");
        }
        else {
            return "black";
        }

        return color;
    }

    function calculateHeading(turtle, value, heading) {
        var angle   = turtle._angle   || 0,
            radians = turtle._radians || 0;

        heading || (heading = {});

        if (typeof value === "number") {
            if (turtle._isRadians) {
                angle = radians = value % Turtle.RADIANS;
            }
            else if (turtle._fullCircle) {
                angle   = (value % turtle._fullCircle);
                radians = angle / turtle._fullCircle * Turtle.RADIANS;
            }
            else {
                angle = radians = 0;
            }

            if (angle < 0) {
                angle   += turtle._fullCircle;
                radians += Turtle.RADIANS;
            }
        }

        heading.angle   = angle;
        heading.radians = radians;

        return heading;
    }

    function pythonToJavascriptFunction(pyValue, scope) {
        return function() {
            var argsJs = Array.prototype.slice.call(arguments),
                argsPy = argsJs.map(
                    function(argJs) {return Sk.ffi.remapToPy(argJs);}
                );

            if (typeof(scope) !== "undefined") {
                argsPy.unshift(scope);
            }
            return Sk.misceval.applyAsync(
                undefined, pyValue, undefined, undefined, undefined, argsPy
            ).catch(Sk.uncaughtException);
        };
    }

    function addModuleMethod(klass, module, method, scopeGenerator) {
        var publicMethodName = method.replace(/^\$/, ""),
            displayName      = publicMethodName.replace(/_\$[a-z]+\$$/i, ""),
            maxArgs          = klass.prototype[method].length,
            minArgs          = klass.prototype[method].minArgs,
            keywordArgs      = klass.prototype[method].keywordArgs,
            returnType       = klass.prototype[method].returnType,
            isSk             = klass.prototype[method].isSk,
            wrapperFn;

        if (minArgs === undefined) {
            minArgs = maxArgs;
        }

        wrapperFn = function() {
            var args     = Array.prototype.slice.call(arguments, 0),
                instance = scopeGenerator ? scopeGenerator() : args.shift().instance,
                i, result, susp, resolution, lengthError;

            if (args < minArgs || args.length > maxArgs) {
                lengthError = minArgs === maxArgs ?
                    "exactly " + maxArgs :
                    "between " + minArgs + " and " + maxArgs;

                throw new Sk.builtin.TypeError(displayName + "() takes " + lengthError + " positional argument(s) (" + args.length + " given)");
            }

            for (i = args.length; --i >= 0;) {
                if (args[i] !== undefined) {
                    if (args[i] instanceof Sk.builtin.func) {
                        args[i] = pythonToJavascriptFunction(args[i]);
                    }
                    else if (args[i] instanceof Sk.builtin.method) {
                        args[i] = pythonToJavascriptFunction(args[i].im_func, args[i].im_self);
                    }
                    else if (args[i] && args[i].$d instanceof Sk.builtin.dict && args[i].instance) {
                        args[i] = args[i].instance;
                    }
                    else {
                        args[i] = Sk.ffi.remapToJs(args[i]);
                    }
                }
            }

            try {
                result = instance[method].apply(instance, args);
            } catch(e) {
                if (window && window.console) {
                    window.console.log("wrapped method failed");
                    window.console.log(e.stack);
                }
                throw e;
            }

            if (result instanceof InstantPromise) {
                result = result.lastResult;
            }

            if (result instanceof Promise) {
                result = result.catch(function(e) {
                    if (window && window.console) {
                        window.console.log("promise failed");
                        window.console.log(e.stack);
                    }
                    throw e;
                });

                susp = new Sk.misceval.Suspension();

                susp.resume = function() {
                    return (resolution === undefined) ?
                        Sk.builtin.none.none$ :
                        Sk.ffi.remapToPy(resolution);
                };

                susp.data = {
                    type: "Sk.promise",
                    promise: result.then(function(value) {
                        resolution = value;
                        return value;
                    })
                };

                return susp;
            }
            else {
                if (result === undefined) return Sk.builtin.none.none$;
                if (isSk) return result;
                if (typeof returnType === "function") {
                    return returnType(result);
                }

                return Sk.ffi.remapToPy(result);
            }
        };

        if (keywordArgs) {
            wrapperFn.co_varnames = keywordArgs.slice();
            // make room for required arguments
            for(var i = 0; i < minArgs; i++) {
                wrapperFn.co_varnames.unshift("");
            }
            if (!scopeGenerator) {
                // make room for the "self" argument
                wrapperFn.co_varnames.unshift("");
            }
        }

        module[publicMethodName] = new Sk.builtin.func(wrapperFn);
    }

    function TurtleWrapper($gbl, $loc) {
        $loc.__init__ = new Sk.builtin.func(function (self) {
            self.instance = new Turtle();
            self.instance.skInstance = self;
        });

        for(var key in Turtle.prototype) {
            if (/^\$[a-z_]+/.test(key)) {
                addModuleMethod(Turtle, $loc, key);
            }
        }
    }

    function ScreenWrapper($gbl, $loc) {
        $loc.__init__ = new Sk.builtin.func(function (self) {
            self.instance = getScreen();
        });

        for(var key in Screen.prototype) {
            if (/^\$[a-z_]+/.test(key)) {
                addModuleMethod(Screen, $loc, key);
            }
        }
    }

    for(var key in Turtle.prototype) {
        if (/^\$[a-z_]+/.test(key)) {
            addModuleMethod(Turtle, _module, key, ensureAnonymous);
        }
    }

    // add Screen method aliases to the main turtle module
    // to allow things like:
    //   import turtle
    //   turtle.mainloop()
    addModuleMethod(Screen, _module, "$mainloop", getScreen);
    addModuleMethod(Screen, _module, "$done", getScreen);
    addModuleMethod(Screen, _module, "$bye", getScreen);
    addModuleMethod(Screen, _module, "$tracer", getScreen);
    addModuleMethod(Screen, _module, "$update", getScreen);
    addModuleMethod(Screen, _module, "$delay", getScreen);
    addModuleMethod(Screen, _module, "$window_width", getScreen);
    addModuleMethod(Screen, _module, "$window_height", getScreen);

    _module.Turtle = Sk.misceval.buildClass(_module, TurtleWrapper, "Turtle", []);
    _module.Screen = Sk.misceval.buildClass(_module, ScreenWrapper, "Screen", []);

    // Calling focus(false) will block turtle key/mouse events
    // until focus(true) is called again or until the turtle DOM target
    // is clicked/tabbed into.
    function focusTurtle(value) {
        if (value !== undefined) {
            _focus = !!value;
            if (_focus) {
                getTarget().focus();
            }
            else {
                getTarget().blur();
            }
        }

        return _focus;
    }

    function resetTurtle() {
        cancelAnimationFrame();
        getScreen().reset();
        getFrameManager().reset();

        while (_target.firstChild) {
            _target.removeChild(_target.firstChild);
        }

        if (_mouseHandler) {
            _mouseHandler.reset();
        }

        _durationSinceRedraw = 0;
        _screenInstance      = undefined;
        _anonymousTurtle     = undefined;
        _mouseHandler        = undefined;
        TURTLE_COUNT         = 0;
    }

    function stopTurtle() {
        cancelAnimationFrame();

        if (_mouseHandler) {
            _mouseHandler.reset();
        }

        _durationSinceRedraw = 0;
        _screenInstance      = undefined;
        _anonymousTurtle     = undefined;
        _mouseHandler        = undefined;
        TURTLE_COUNT         = 0;
    }

    return {
        skModule : _module,
        reset    : resetTurtle,
        stop     : stopTurtle,
        focus    : focusTurtle,
        Turtle   : Turtle,
        Screen   : Screen
    };
}

// See if the TurtleGraphics module has already been loaded
// for the currently configured DOM target element.
var currentTarget = getConfiguredTarget();

if (!currentTarget.turtleInstance) {
    currentTarget.turtleInstance = generateTurtleModule(currentTarget);
}
else {
    currentTarget.turtleInstance.reset();
}

Sk.TurtleGraphics.module = currentTarget.turtleInstance.skModule;
Sk.TurtleGraphics.reset  = currentTarget.turtleInstance.reset;
Sk.TurtleGraphics.stop   = currentTarget.turtleInstance.stop;
Sk.TurtleGraphics.focus  = currentTarget.turtleInstance.focus;
Sk.TurtleGraphics.raw = {
    Turtle : currentTarget.turtleInstance.Turtle,
    Screen : currentTarget.turtleInstance.Screen
};

return currentTarget.turtleInstance.skModule;

};