/*
* 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);
}
}
};
};