In the last chapter, we have created the white page where we can paint on. In this chapters we are starting to draw lines. The process in PostScript is always defined points that form a path of a polygon (moveto, lineto), than either draw the border of the polygon (stroke) or paint the polygon (fill) and then finish the page (shwopage).
While building the path, the interpreter needs a place to store the path and also all other parameters that could be of importance (color, stroke width). We therefore add to the context a graphics object to hold that state.
context.graphics = { path: [], current: [], color: [0, 0, 0, 255]};
And we adapt the console to show the graphics state.
Run
postScriptEditor = function(code) {
const id = Math.floor(Math.random() * 1000); // create unique id for console.log
const node = document.createElement("DIV"); // build the HTML nodes
node.id = "id" & id;
node.className = "psmain";
const node2 = document.createElement("DIV");
node2.className = "editzone";
node.appendChild(node2);
const node3 = document.createElement("DIV");
node3.className = "editheader";
node3.innerHTML = "PostScript";
node2.appendChild(node3);
const node4 = document.createElement("FORM");
node2.appendChild(node4);
const node5 = document.createElement("BUTTON");
node5.type = "button";
node5.id = "button" + id;
node5.innerHTML = "Run";
node4.appendChild(node5);
const node6 = document.createElement("TEXTAREA");
node6.id = "editor" + id;
node6.className = "pseditor";
node6.innerHTML = code;
node6.rows = code.split(endofline).length + 1;
node4.appendChild(node6);
const node7 = document.createElement("DIV");
node7.id = "console" + id;
node7.className = "jsconsole";
node.appendChild(node7);
const node8 = document.createElement("CANVAS");
node8.id = "canvas" + id;
node8.className = "jscanvas";
node8.width = 590;
node8.height = 330;
node.appendChild(node8);
console.log(node.outerHTML); // add the node to the parent element
const script = `redirectConsole($id);
run$id = function() {
redirectConsole($id);
const width = 590;
const height = 330;
const data = new Uint8ClampedArray(width * height * 4);
// make it white
for(i = 0; i< data.length; i++) data[i] = 255;
const code = document.getElementById("editor$id").value;
var context = { data: data , width: width, height: height };
context = rpn(code, context);
console.log("stack: " + context.stack.reduce((acc,v) => acc + v.dump + " " , " "));
if (context.lasterror) {
console.log("rpn: " + context.currentcode);
}
for (key in context.graphics) {
console.log(key + ": " + JSON.stringify(context.graphics[key]));
}
for (key in context.dict) {
console.log(key + " = " + (context.dict[key].dump));
}
if (context.heap.length) {
console.log("heap: " + context.heap.reduce( function(acc, v) {
const val = v.value;
if (Array.isArray(val)) {
return acc + " [" + val.reduce( (acc2, v2) => acc2 + v2.dump + " " , " ") + "] "+v.counter+" ";
} else if (val === null) {
return acc + " ";
} else {
return acc + " (" + val + ") "+v.counter;
}
} , ""));
}
const image = new ImageData(context.data, width);
const canvas = document.getElementById("canvas$id");
ctx = canvas.getContext("2d");
ctx.putImageData(image, 0,0);
};
document.getElementById("button$id").onclick = run$id;`.replaceAll("$id", id).replaceAll("$code",code);
const scriptnode = document.createElement("SCRIPT");
scriptnode.innerHTML = script;
document.body.appendChild(scriptnode); // run the script
};
We build our first operators to create paths.
x y moveto
x y rmoveto
x y lineto
x y rlineto
closepath
g setgray
The path we constructs is an array of subpaths, which contain line segments. Each line segment has a type, then coordinates. For the moment, we only have a straight lines, later we will also have curved lines.
Why the subpaths: There are some graphics elements which have subpaths like the letter P oder the letter A. Subpaths will later allow in fill to exclude regions from fill.
Run
operators.currentpoint = function(context) {
context.stack.push(new rpnNumber(context.graphics.current[0]));
context.stack.push(new rpnNumber(context.graphics.current[1]));
return context;
};
operators.moveto = function(context) {
const [y, x] = context.pop("number","number");
if (!y) return context;
context.graphics.path.push([]);
context.graphics.current = [ x.value, y.value ];
return context;
};
unitTest("100 50 moveto","");
unitTest("100 50 moveto currentpoint","100 50");
unitTest("(a) 1 moveto","!typeerror");
unitTest("1 moveto","1 !stackunderflow");
unitTest("moveto","!stackunderflow");
operators.rmoveto = function(context) {
const [y, x] = context.pop("number","number");
if (!y) return context;
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
} else {
context.graphics.path.push([]);
context.graphics.current = [ context.graphics.current[0] + x.value, context.graphics.current[1] + y.value ];
}
return context;
};
unitTest("100 50 moveto 50 50 rmoveto","");
unitTest("100 50 moveto 50 50 rmoveto currentpoint","150 100");
unitTest("50 50 rmoveto","!nocurrentpoint");
unitTest("(a) 1 rmoveto","!typeerror");
unitTest("1 rmoveto","1 !stackunderflow");
unitTest("rmoveto","!stackunderflow");
operators.lineto = function(context) {
const [y, x] = context.pop("number","number");
if (!y) return context;
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
} else if (!context.graphics.path.length) {
context.stack.push(new rpnError("nocurrentpath"));
} else {
const subpath = context.graphics.path.pop();
subpath.push([ "L", context.graphics.current[0], context.graphics.current[1], x.value, y.value ]);
context.graphics.path.push(subpath);
context.graphics.current = [ x.value, y.value ];
}
return context;
};
unitTest("100 50 moveto 50 50 lineto",'');
unitTest("100 50 moveto 50 50 lineto currentpoint","50 50");
unitTest("50 50 lineto","!nocurrentpoint");
unitTest("(a) 1 lineto","!typeerror");
unitTest("1 lineto","1 !stackunderflow");
unitTest("lineto","!stackunderflow");
operators.rlineto = function(context) {
const [y, x] = context.pop("number","number");
if (!y) return context;
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
} else if (!context.graphics.path.length) {
context.stack.push(new rpnError("nocurrentpath"));
} else {
const subpath = context.graphics.path.pop();
subpath.push([ "L", context.graphics.current[0], context.graphics.current[1], context.graphics.current[0] + x.value, context.graphics.current[1] + y.value ]);
context.graphics.path.push(subpath);
context.graphics.current = [ context.graphics.current[0] + x.value, context.graphics.current[1] + y.value ];
}
return context;
};
unitTest("100 50 moveto 50 50 rlineto",'');
unitTest("100 50 moveto 50 50 rlineto currentpoint","150 100");
unitTest("50 50 rlineto","!nocurrentpoint");
unitTest("(a) 1 rlineto","!typeerror");
unitTest("1 rlineto","1 !stackunderflow");
unitTest("rlineto","!stackunderflow");
operators.closepath = function(context) {
if (!context.graphics.current.length) {
context.stack.push(new rpnError("nocurrentpoint"));
} else if (!context.graphics.path.length) {
context.stack.push(new rpnError("nocurrentpath"));
} else {
if (context.graphics.path.length) {
const subpath = context.graphics.path.pop();
const x0 = subpath[0][1];
const y0 = subpath[0][2];
subpath.push( [ "L", context.graphics.current[0], context.graphics.current[1], x0, y0 ]);
context.graphics.path.push(subpath);
context.graphics.current = [ x0, y0 ];
}
}
return context;
};
unitTest("100 50 moveto 50 150 lineto closepath",'');
unitTest("100 50 moveto 50 150 lineto closepath currentpoint","100 50");
unitTest("50 50 closepath","50 50 !nocurrentpoint");
operators.setgray = function(context) {
const [g] = context.pop("number");
if (!g) return context;
const glimited = Math.min(Math.max(g.value,0),1);
context.graphics.color = [ glimited * 255, glimited * 255, glimited * 255, 255 ];
return context;
};
unitTest("0 setgray","");
unitTest("1 setgray","");
unitTest("-1 setgray","");
unitTest("3 setgray","");
unitTest("(ab) setgray","!typeerror");
unitTest("setgray","!stackunderflow");
We can now build a path of two triangles. The first triangle is a simple triangle with absolute positions. We then define an operator that uses relative triangles. This allows us to position the same triangle over the page.
Run
postScriptEditor(`100 100 moveto 200 100 lineto 150 200 lineto closepath
/triangle { moveto 100 0 rlineto -50 100 rlineto closepath } def
200 100 triangle
240 140 triangle
280 180 triangle
` )
We have the paths, now we need to draw the pixels. We define the operators to draw and to output the page.
We have to draw a line from x0,y0 to x1,y1. We use linear interpolation. How many points do we need. We want to be sure that every x and every y is drawn, so we define the number of steps as the maximum of the horizontal and the vertical distance.
Run
operators.stroke = function(context) {
for (var subpath of context.graphics.path) {
for (var line of subpath) {
const type = line[0]; // we will have other types
const x0 = line[1];
const y0 = line[2];
const x1 = line[3];
const y1 = line[4];
const dx = x1 - x0;
const dy = y1 - y0;
const steps = Math.max(Math.abs(dx), Math.abs(dy));
if (steps) {
const ex = (x1 - x0) / steps;
const ey = (y1 - y0) / steps;
var x = x0;
var y = y0;
for (var i = 0; i < steps; i++) {
offset = ((context.height - 1 - Math.round(y)) * context.width + Math.round(x) ) * 4;
for (var c = 0; c < 4; c++) {
context.data[offset + c] = context.graphics.color[c];
}
x += ex;
y += ey;
}
}
}
}
context.graphics.path = [];
context.graphics.current = [];
return context;
};
operators.showpage = function(context) {
// in our current context, this operator does nothing
return context;
};
We add stroke to the PostScript code
Run
postScriptEditor(`100 100 moveto 200 100 lineto 150 200 lineto closepath
/triangle { moveto 100 0 rlineto -50 100 rlineto closepath } def
200 100 triangle
240 140 triangle
280 180 triangle
stroke
showpage
` )
That looks already very interesting. We did draw all paths in a single stroke. This is possible, but normally we draw it seperately, especially if they have different colors so normally we would stroke each path separately.
Run
postScriptEditor(`100 100 moveto 200 100 lineto 150 200 lineto closepath stroke
/triangle { moveto 100 0 rlineto -50 100 rlineto closepath } def
0.2 setgray 200 100 triangle stroke
0.4 setgray 240 140 triangle stroke
0.6 setgray 280 180 triangle stroke
showpage
` )
We can draw complex things like letters
Run
postScriptEditor(`/t { moveto 30 0 rmoveto 10 0 rlineto 0 50 rlineto 20 0 rlineto 0 10 rlineto -50 0 rlineto 0 -10 rlineto 20 0 rlineto closepath } def
/a { moveto 10 0 rlineto 10 20 rlineto 20 00 rlineto 10 -20 rlineto 10 0 rlineto -30 60 rlineto closepath 25 30 rmoveto 10 0 rlineto -5 10 rlineto closepath } def
100 100 t stroke
150 100 a stroke
` )
We have made big steps today. But we are only drawing the edges. In the next chapter, we will introduce the scanfill algorithm to fill a a path.
The codebase is now 1125 lines ps20240713.js
My Journey to PostScript