We can now start to code. First we add the transformation matrix to context.graphics. As the context is becoming larger and larger, we profit to remove it from the rpn function and create an own class it. All but trivial manipulations of the context will be delegated to the context. We must also adapt postScriptEditor and profit to move the canvas above, as the console output gets longer.

We add the operators that work with the matrix

Run
operators.currentmatrix = function(context) {
const m = [];
for (v of context.graphics.matrix) {
m.push(new rpnNumber(v));
}
context.stack.push(new rpnArray(m, context));
return context;
};
unitTest("currentmatrix","[1 0 0 1 0 0]");
operators.scale = function(context) {
const [y, x] = context.pop("number","number");
if (!x) return context;
context.graphics.matrix[0] *= x.value;
context.graphics.matrix[1] *= y.value;
context.graphics.matrix[2] *= x.value;
context.graphics.matrix[3] *= y.value;
return context;
};
unitTest("2 2 scale currentmatrix","[2 0 0 2 0 0]");
unitTest("2 (a) scale currentmatrix","2 !typeerror");
unitTest("2 scale currentmatrix","2 !stackunderflow");
operators.rotate = function(context) {
const [a] = context.pop("number");
if (!a) return context;
const v = a.value * 3.1415326536 / 180 * -1;
const m = context.graphics.matrix.slice();
context.graphics.matrix[0] = Math.cos(v)*m[0] - Math.sin(v)*m[2];
context.graphics.matrix[1] = Math.cos(v)*m[1] - Math.sin(v)*m[3];
context.graphics.matrix[2] = Math.sin(v)*m[0] + Math.cos(v)*m[2];
context.graphics.matrix[3] = Math.sin(v)*m[1] + Math.cos(v)*m[3];
return context;
};
unitTest("60 rotate currentmatrix","[0.5000173204051283 0.8660154036129353 -0.8660154036129353 0.5000173204051283 0 0]");
unitTest("(a) rotate currentmatrix","!typeerror");
unitTest("rotate currentmatrix","!stackunderflow");
operators.translate = function(context) {
const [y, x] = context.pop("number","number");
if (!x) return context;
const m = context.graphics.matrix.slice();
context.graphics.matrix[4] += x.value*m[0] + y.value*m[2];
context.graphics.matrix[5] += x.value*m[1] + y.value*m[3];
return context;
};
unitTest("50 100 translate currentmatrix","[1 0 0 1 50 100]");
unitTest("2 (a) translate currentmatrix","2 !typeerror");
unitTest("2 translate currentmatrix","2 !stackunderflow");
operators.transform = function(context) {
const [y, x] = context.pop("number","number");
if (!x) return context;
const [x2, y2] = context.transform(x.value,y.value);
context.stack.push(new rpnNumber(x2));
context.stack.push(new rpnNumber(y2));
return context;
};
unitTest("2 2 translate 50 100 transform","52 102");
unitTest("50 100 transform","50 100");
unitTest("(a) 100 transform","!typeerror");
unitTest("50 transform","50 !stackunderflow");
operators.itransform = function(context) {
const [y, x] = context.pop("number","number");
if (!x) return context;
const [x2, y2] = context.itransform(x.value,y.value);
context.stack.push(new rpnNumber(x2));
context.stack.push(new rpnNumber(y2));
return context;
};
unitTest("2 2 translate 52 102 itransform","50 100");
unitTest("50 100 itransform","50 100");
unitTest("(a) 100 itransform","!typeerror");
unitTest("50 itransform","50 !stackunderflow");
operators.setmatrix = function(context) {
const [m] = context.pop("array");
if (!m) return context;
if (m.value.length != 6) {
return context.error("typeerror");
}
for (var i = 0; i < 6; i++) {
context.graphics.matrix[i] = m.value[i].value;
}
return context;
};
unitTest("[1 2 3 4 5 6] setmatrix currentmatrix","[1 2 3 4 5 6]");

And we must now integrate transform in all operators that may manipulate user space coordinates.

Run
operators.curveto = function(context) {
const [y3, x3, y2, x2, y1, x1] = context.pop("number","number","number","number","number","number");
if (!y3) 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();
const [x1t, y1t] = context.transform(x1.value, y1.value);
const [x2t, y2t] = context.transform(x2.value, y2.value);
const [x3t, y3t] = context.transform(x3.value, y3.value);
subpath.push([ "C", context.graphics.current[0], context.graphics.current[1], x1t, y1t, x2t, y2t, x3t, y3t ]);
context.graphics.path.push(subpath);
context.graphics.current = [ x3t, y3t ];
}
return context;
};
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();
const [xt, yt] = context.transform(x.value, y.value);
subpath.push([ "L", context.graphics.current[0], context.graphics.current[1], xt, yt]);
context.graphics.path.push(subpath);
context.graphics.current = [ xt, yt ];
}
return context;
};
operators.moveto = function(context) {
const [y, x] = context.pop("number","number");
if (!y) return context;
context.graphics.path.push([]);
const [xt, yt] = context.transform(x.value, y.value);
context.graphics.current = [ xt, yt ];
return context;
};
operators.rcurveto = function(context) {
const [y3, x3, y2, x2, y1, x1] = context.pop("number","number","number","number","number","number");
if (!y3) 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();
const [xct, yct] = context.itransform(context.graphics.current[0], context.graphics.current[1]);
const [x1t, y1t] = context.transform(xct + x1.value, yct + y1.value);
const [x2t, y2t] = context.transform(xct + x2.value, yct + y2.value);
const [x3t, y3t] = context.transform(xct + x3.value, yct + y3.value);
subpath.push([ "C", context.graphics.current[0], context.graphics.current[1], x1t, y1t, x2t, y2t, x3t, y3t]);
context.graphics.path.push(subpath);
context.graphics.current = [ x3t, y3t ];
}
return context;
};
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();
const [xct, yct] = context.itransform(context.graphics.current[0], context.graphics.current[1]);
const [xt, yt] = context.transform(xct + x.value, yct + y.value);
subpath.push([ "L", context.graphics.current[0], context.graphics.current[1], xt, yt ]);
context.graphics.path.push(subpath);
context.graphics.current = [ xct, yct ];
}
return context;
};
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([]);
const [xct, yct] = context.itransform(context.graphics.current[0], context.graphics.current[1]);
const [xt, yt] = context.transform(xct + x.value, yct + y.value);
context.graphics.current = [ xt, yt ];
}
return context;
};

Let's see.

Run
postScriptEditor(`/reset { [1 0 0 1 0 0] setmatrix } def
/t { 30 0 moveto 40 0 lineto 40 50 lineto 60 50 lineto 60 60 lineto 10 60 lineto 10 50 lineto 30 50 lineto closepath 50 0 translate } def
/a { 0 0 moveto 10 0 lineto 20 20 lineto 40 20 lineto 50 0 lineto 60 0 lineto 30 60 lineto closepath 25 30 moveto 35 30 lineto 30 40 lineto closepath 50 0 translate } def
/o { 30 0 moveto 44 0 50 10 50 30 curveto
50 50 44 60 30 60 curveto
16 60 10 50 10 30 curveto
10 10 16 0 30 0 curveto
30 10 moveto 37 10 40 17 40 30 curveto
40 44 37 50 30 50 curveto
23 50 20 44 20 30 curveto
20 16 26 10 30 10 curveto
} def
2 2 scale t a o 0.5 setgray fill reset
100 100 translate t a o 0 setgray fill reset
30 rotate t a o 0.7 setgray fill reset`)

Seems to work, but it looks like we have a problem with scanFill. The rest of the T on the left is appearing on the right. We fix that next chapter.

The codebase has now 1540 lines ps20240728.js

My Journey to PostScript