git @ Cat's Eye Technologies Pixley / master impl / pixley.js / src / pixley-depictor.js
master

Tree @master (Download .tar.gz)

pixley-depictor.js @masterraw · history · blame

/*
 * Depicts a Pixley program (or, really, any S-expression) as a colourful
 * set of nested rectangles.
 *
 * requires pixley.js
 */

/*
 * We want to decorate S-expressions with information about where they're
 * depicted and what size they are, so we can't just use `null` for the
 * empty list.
 */
var EmptyList = function() {
    this.toString = function() {
        return '';
    };
};

function PixleyDepictor() {
    var canvas;
    var ctx;
    var margin = 3;
    var blockSize = 10;

    this.init = function(c) {
        canvas = c;
        ctx = canvas.getContext("2d");
        this.colourMap = {
            'car':    '#6949d7',
            'cdr':    '#1f0772',
            'cond':   'yellow',
            'cons':   '#3714b0',
            'else':   'red',
            'equal?': 'green',
            'lambda': '#6f0aaa',
            'let*':   'brown',
            'list?':  'aquamarine',
            'quote':  'purple',
        };
        
        this.availableColours = [
            '#00c0c0',
            '#c000c0',
            '#c0c000',
            '#00a0a0',
            '#a000a0',
            '#a0a000',
            '#008080',
            '#800080',
            '#808000',
            '#006060',
            '#600060',
            '#606000',
            '#004040',
            '#400040',
            '#404000',
            '#002020',
            '#200020',
            '#202000',

            '#00ffa0',
            '#ff00a0',
            '#ffffa0',
            '#00a0ff',
            '#ffa000',
            '#ffa0ff',
            '#00c0ff',
            '#c000ff',
            '#c0c0ff',
            '#00ffc0',
            '#c0ff00',
            '#c0ffc0',
            '#00c0a0',
            '#c000a0',
            '#c0c0a0',
            '#00a0c0',
            '#c0a000',
            '#c0a0c0',
            '#0080a0',
            '#8000a0',
            '#8080a0',
            '#00a080',
            '#80a000',
            '#80a080',
            '#0040a0',
            '#4000a0',
            '#4040a0',
            '#00a040',
            '#40a000',
            '#40a040'
        ];
        this.availableIndex = 0;
    };

    this.getColour = function(text) {
        var entry = this.colourMap[text];
        if (entry) return entry;
        if (this.availableIndex >= this.availableColours.length) {
            //alert('Ran out of unique colours!');
            this.availableIndex = 0;
        }
        entry = this.availableColours[this.availableIndex];
        this.colourMap[text] = entry;
        this.availableIndex++;
        return entry;
    };

    this.depict = function(sexp) {
        if (!sexp) return;
        this.availableIndex = 0;
        sexp = cloneSexp(sexp);
        canvas.style.display = "block";
        this.transformNullsToEmptyLists(sexp);
        this.decorateSexp(0, 0, sexp);
        canvas.width = sexp.width;
        canvas.height = sexp.height;
        // this is implied by the canvas size change:
        // ctx.clearRect(0, 0, canvas.width, canvas.height);
        this.depictSexp(0, 0, sexp);
    };

    // this method had side-effects; it modified sexp in-place
    this.transformNullsToEmptyLists = function(sexp) {
        if (sexp === null) {
            // what can we do?  this shouldn't happen
            return;
        } else if (sexp.text === undefined) {
            while (sexp !== null) {
                if (sexp.head === null) {
                    sexp.head = new EmptyList();
                } else {
                    this.transformNullsToEmptyLists(sexp.head);
                }
                sexp = sexp.tail;
            }
        } else {
            return;
        }
    };

    /*
     * Decorate all Cons and Atom cells in the s-expression with
     * some details about how to depict them on the canvas, mainly
     * the width and the height which the s-expression will occupy.
     */
    this.decorateSexp = function(x, y, sexp, parentOrientation) {
        parentOrientation = !!parentOrientation; // ensure it is a boolean
        /*
         * Determine if we have a Cons cell or an Atom.
         */
        if (sexp instanceof EmptyList) {
            /*
             * Empty list.  Fill in width and height.
             */
            sexp.width = blockSize;
            sexp.height = blockSize;
        } else if (sexp.text === undefined) {
            /*
             * Cons cell.
             * First, determine if it has an atom at its head.
             */
            var head = sexp.head;
            if (head.text !== undefined) { // head entry is an atom
                sexp.startsWithAtom = true;
            }
            /*
             * Next, find the extents of the children, then derive
             * the extents of the cons cell, and fill them in.
             */
            var children = [];
            var origSexp = sexp;
            if (sexp.startsWithAtom) {
                // for the purposes of determining the bounding box size,
                // skip the head atom, as we'll be drawing the entire list
                // in that colour -- unless the list contains *only* the
                // head atom, in which case, we still want to draw that.
                // TODO: maybe handle this better; special case, smaller box.
                if (sexp.tail !== null) {
                    sexp = sexp.tail;
                }
            }
            while (sexp != null) {
                children.push(sexp.head);
                sexp = sexp.tail;
            }
            var len = children.length;

            /*
             * Determine the orientation of this sexp.
             */

            // STYLE 1: only cond and let* bindings are vertical
            origSexp.horizontal = true;
            if (!origSexp.startsWithAtom) {
                origSexp.horizontal = false;
            }
            if (origSexp.startsWithAtom && head.text === 'cond') {
                origSexp.horizontal = false;
            }
            
            // STYLE 2: orientation is opposite of parent's
            /*
            origSexp.horizontal = !parentOrientation;
            */
            
            // STYLE 3: orientation is random!
            /*
            origSexp.horizontal = (Math.random() > 0.5 ? true : false);
            */

            for (var i = 0; i < len; i++) {
                this.decorateSexp(x, y, children[i], origSexp.horizontal);
            }

            var w = 0;
            var h = 0;
            for (var i = 0; i < len; i++) {
                // alert(i + '...' + w);
                if (origSexp.horizontal) {
                    w += children[i].width || 0;
                    if (children[i].height + margin * 2 > h) {
                        h = children[i].height + margin * 2;
                    }
                } else {
                    h += children[i].height || 0;
                    if (children[i].width + margin * 2 > w) {
                        w = children[i].width + margin * 2;
                    }
                }
            }
            if (origSexp.horizontal) {
                w = w + margin * (len + 1);
            } else {
                h = h + margin * (len + 1);
            }
            origSexp.width = w;
            origSexp.height = h;
        } else {
            /*
             * Atom.  Fill in width and height.
             */
            sexp.width = blockSize;
            sexp.height = blockSize;
        }
    };

    /*
     * Recursively depict this s-expression on the canvas.
     */
    this.depictSexp = function(x, y, sexp, parentColour) {
        /*
         * Determine if we have a Cons cell or an Atom.
         */
        if (sexp instanceof EmptyList) {
            /*
             * Empty list.  Fill the rect in white, w/black border.
             */
            ctx.fillStyle = 'white';
            ctx.fillRect(x - 0.5, y - 0.5, sexp.width, sexp.height);
            ctx.strokeStyle = 'black';
            ctx.strokeRect(x - 0.5, y - 0.5, sexp.width, sexp.height);
        } else if (sexp.text === undefined) {
            /*
             * Cons cell.  Get the list into a more Javascript-y data structure.
             */
            var origSexp = sexp;

            var children = [];
            if (sexp.startsWithAtom) {
                sexp = sexp.tail;
            }
            while (sexp != null) {
                children.push(sexp.head);
                sexp = sexp.tail;
            }
            var len = children.length;

            var colour = 'white';
            if (origSexp.startsWithAtom) {
               // If there's a head atom, fill in rect with head atom's colour
               colour = this.getColour(origSexp.head.text);
            }            
            ctx.fillStyle = colour;
            ctx.fillRect(x - 0.5, y - 0.5, origSexp.width, origSexp.height);
            ctx.strokeStyle = "black";
            ctx.lineWidth = 1;
            ctx.strokeRect(x - 0.5, y - 0.5, origSexp.width, origSexp.height);

            var innerX = x + margin;
            var innerY = y + margin;
            for (var i = 0; i < len; i++) {
                if (children[i] === null) {
                    continue;
                }
                this.depictSexp(innerX, innerY, children[i], colour);
                if (origSexp.horizontal) {
                    innerX += children[i].width + margin;
                } else {
                    innerY += children[i].height + margin;
                }
            }
        } else {
            /*
             * Atom.  Fill the rect in the atom's colour.
             */
            var colour = this.getColour(sexp.text);
            ctx.fillStyle = colour;
            ctx.fillRect(x - 0.5, y - 0.5, sexp.width, sexp.height);
            if (colour === parentColour) {
                ctx.strokeStyle = 'white';
                ctx.strokeRect(x - 0.5, y - 0.5, sexp.width, sexp.height);
            }
        }
    };
};