We have a problem with stroke. As you remember from A bug in the stroke algorithm , we designed the stroke operator pointing multiple times at the same space, to simplify the paths and avoid intersection problems. However, if you paint multiple times with transparency, this becomes a visible artefact.
We can fix this by adding a transfer layer. We do not paint directly to the page, but create an auxiliary bitmap where we paint opaque, and then we apply a copy with transparency from the transfer layer to the page. So we modify the rpnRawDevice. Run it and then the PostScript code again.
Run
rpnRawDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
if (node) this.initgraphics(node.width, node.height, 1);
}
initgraphics(width, height, oversampling, transparent) {
this.data = new Uint8ClampedArray(width * height * 4 * oversampling * oversampling);
if (!transparent) {
for (let i = 0; i < this.data.length; i++) this.data[i] = 255;
}
this.transferdata = new Uint8ClampedArray(width * height * 4 * oversampling * oversampling);
for (let i = 0; i < this.transferdata.length; i++) this.transferdata[i] = 0;
this.node.width = width;
this.node.height = height;
const ctx = this.node.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
}
fill(context) {
if (context.device.raw + context.device.rawurl < 1) return context;
const flatpath = [];
for (let subpath of context.graphics.path) {
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
flatpath.push(elem);
}
} else {
flatpath.push(line);
}
}
}
this.data = scanFill(flatpath, context.width, context.height, context.graphics.color, this.data);
return context;
}
stroke(context) {
if (context.device.raw + context.device.rawurl < 1) return context;
const w = context.graphics.linewidth / 2;
const ad = Math.PI / 2;
if (!w) return context;
const transfercolor = context.graphics.color.slice();
transfercolor[3] = 255;
for (let subpath of context.graphics.path) {
var subflatpath = [];
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
subflatpath.push(elem);
}
} else {
subflatpath.push(line);
}
}
var olda = [];
var oldb = [];
if (!subflatpath.length) continue;
if (subflatpath[0][1] == subflatpath[subflatpath.length-1][3] && subflatpath[0][2] == subflatpath[subflatpath.length-1][4])
subflatpath.push(subflatpath[0]);
for (let line of subflatpath) {
const [type, x0, y0, x1, y1] = line;
const a = Math.atan2(y1 - y0, x1 - x0);
const x0a = x0 + Math.cos(a - ad) * w;
const y0a = y0 + Math.sin(a - ad) * w;
const x1a = x1 + Math.cos(a - ad) * w;
const y1a = y1 + Math.sin(a - ad) * w;
const x0b = x0 + Math.cos(a + ad) * w;
const y0b = y0 + Math.sin(a + ad) * w;
const x1b = x1 + Math.cos(a + ad) * w;
const y1b = y1 + Math.sin(a + ad) * w;
this.transferdata = scanFill([ ["L",x0a, y0a, x1a, y1a], ["L",x0a, y0a, x0b, y0b],["L", x0b, y0b, x1b, y1b], ["L",x1a, y1a, x1b, y1b]], context.width, context.height, transfercolor, this.transferdata);
if (olda.length) {
const [xa, ya] = lineIntersection(olda[1],olda[2],olda[3],olda[4],x0a, y0a, x1a, y1a);
const [xb, yb] = lineIntersection(oldb[1],oldb[2],oldb[3],oldb[4],x0b, y0b, x1b, y1b);
if (xa !== null && xb !== null) {
this.transferdata = scanFill([ ["L",olda[3],olda[4], xa, ya], ["L",xa, ya, x0a, y0a],["L", x0a, y0a, x0b, y0b], ["L",x0b, y0b, xb, yb],["L",xb, yb, oldb[3], oldb[4]],["L", oldb[3], oldb[4], olda[3], olda[4]]], context.width, context.height, transfercolor, this.transferdata);
}
}
olda = ["L", x0a, y0a, x1a, y1a];
oldb = ["L", x0b, y0b, x1b, y1b];
}
}
// apply transfer to data
for (let pixel = 0; pixel < this.transferdata.length; pixel += 4) {
const bg = this.data.slice(pixel, pixel + 4);
const fg = context.graphics.color.slice();
if ( this.transferdata[pixel + 3] ) {
const da = fg[3]/255.0 + bg[3]/255.0 * (1 - fg[3]/255.0);
for (let c = 0; c < 3; c++) {
fg[c] = fg[c] * fg[c]/255.0 + bg[c] * bg[3] / 255.0 * (1 - fg[3]/255.0);
if (da) fg[c] /= da;
this.data[pixel + c] = fg[c];
}
this.data[pixel + 3] = 255.0 * da;
}
}
// clear transferdata
for (let i = 0; i < this.transferdata.length; i++) this.transferdata[i] = 0;
return context;
}
showpage(context) {
if (context.device.raw + context.device.rawurl < 1) {
this.node.style.display = "none";
if (this.urlnode) this.urlnode.style.display = "none";
return context;
}
this.node.style.display = (context.device.raw) ? "block" : "none";
const image = new ImageData(this.data, context.width * context.device.oversampling);
const canvas = (this.node) ? this.node : document.createElement("CANVAS");
const ctx = canvas.getContext("2d");
if (context.device.oversampling > 1) {
var nodebig = document.createElement("CANVAS");
nodebig.width = context.width * context.device.oversampling;
nodebig.height = context.height * context.device.oversampling;
nodebig.getContext("2d").putImageData(image, 0,0);
ctx.save();
ctx.scale(1/context.device.oversampling, 1/context.device.oversampling);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(nodebig,0,0);
ctx.restore();
} else {
ctx.putImageData(image, 0,0);
}
if (this.urlnode && context.device.rawurl) {
this.urlnode.style.display = "block";
const url = canvas.toDataURL();
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.png");
} else {
if (this.urlnode) this.urlnode.style.display = "none";
}
return context;
}
};
If you want to go back, run this old class definition again
Run
rpnRawDevice = class {
constructor(node, urlnode) {
this.node = node;
if (!node) document.createElement("CANVAS");
this.urlnode = urlnode;
if (node) this.initgraphics(node.width, node.height, 1, 0);
}
initgraphics(width, height, oversampling, transparent) {
this.data = new Uint8ClampedArray(width * height * 4 * oversampling * oversampling);
if (!transparent) {
for (let i = 0; i < this.data.length; i++) this.data[i] = 255;
}
this.node.width = width;
this.node.height = height;
const ctx = this.node.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
}
fill(context) {
if (context.device.raw + context.device.rawurl < 1) return context;
const flatpath = [];
for (let subpath of context.graphics.path) {
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
flatpath.push(elem);
}
} else {
flatpath.push(line);
}
}
}
this.data = scanFill(flatpath, context.width, context.height, context.graphics.color, this.data);
return context;
}
stroke(context) {
if (context.device.raw + context.device.rawurl < 1) return context;
const w = context.graphics.linewidth / 2;
const ad = Math.PI / 2;
if (!w) return context;
for (let subpath of context.graphics.path) {
var subflatpath = [];
for (let line of subpath) {
if (line[0] == "C") {
const lines = bezier(line,-1);
for (let elem of lines) {
subflatpath.push(elem);
}
} else {
subflatpath.push(line);
}
}
var olda = [];
var oldb = [];
if (!subflatpath.length) continue;
if (subflatpath[0][1] == subflatpath[subflatpath.length-1][3] && subflatpath[0][2] == subflatpath[subflatpath.length-1][4])
subflatpath.push(subflatpath[0]);
for (let line of subflatpath) {
const [type, x0, y0, x1, y1] = line;
const a = Math.atan2(y1 - y0, x1 - x0);
const x0a = x0 + Math.cos(a - ad) * w;
const y0a = y0 + Math.sin(a - ad) * w;
const x1a = x1 + Math.cos(a - ad) * w;
const y1a = y1 + Math.sin(a - ad) * w;
const x0b = x0 + Math.cos(a + ad) * w;
const y0b = y0 + Math.sin(a + ad) * w;
const x1b = x1 + Math.cos(a + ad) * w;
const y1b = y1 + Math.sin(a + ad) * w;
this.data = scanFill([ ["L",x0a, y0a, x1a, y1a], ["L",x0a, y0a, x0b, y0b],["L", x0b, y0b, x1b, y1b], ["L",x1a, y1a, x1b, y1b]], context.width, context.height, context.graphics.color, this.data);
if (olda.length) {
const [xa, ya] = lineIntersection(olda[1],olda[2],olda[3],olda[4],x0a, y0a, x1a, y1a);
const [xb, yb] = lineIntersection(oldb[1],oldb[2],oldb[3],oldb[4],x0b, y0b, x1b, y1b);
if (xa !== null && xb !== null) {
this.data = scanFill([ ["L",olda[3],olda[4], xa, ya], ["L",xa, ya, x0a, y0a],["L", x0a, y0a, x0b, y0b], ["L",x0b, y0b, xb, yb],["L",xb, yb, oldb[3], oldb[4]],["L", oldb[3], oldb[4], olda[3], olda[4]]], context.width, context.height, context.graphics.color, this.data);
}
}
olda = ["L", x0a, y0a, x1a, y1a];
oldb = ["L", x0b, y0b, x1b, y1b];
}
}
return context;
}
showpage(context) {
if (context.device.raw + context.device.rawurl < 1) {
this.node.style.display = "none";
if (this.urlnode) this.urlnode.style.display = "none";
return context;
}
this.node.style.display = (context.device.raw) ? "block" : "none";
const image = new ImageData(this.data, context.width * context.device.oversampling);
const canvas = (this.node) ? this.node : document.createElement("CANVAS");
const ctx = canvas.getContext("2d");
if (context.device.oversampling > 1) {
var nodebig = document.createElement("CANVAS");
nodebig.width = context.width * context.device.oversampling;
nodebig.height = context.height * context.device.oversampling;
nodebig.getContext("2d").putImageData(image, 0,0);
ctx.save();
ctx.scale(1/context.device.oversampling, 1/context.device.oversampling);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(nodebig,0,0);
ctx.restore();
} else {
ctx.putImageData(image, 0,0);
}
if (this.urlnode && context.device.rawurl) {
this.urlnode.style.display = "block";
const url = canvas.toDataURL();
this.urlnode.href = url;
this.urlnode.setAttribute("download", "PS.png");
} else {
if (this.urlnode) this.urlnode.style.display = "none";
}
return context;
}
}
ps20241101.js 3473 lines
https://www.belle-nuit.com/site/files/minimal7.html
My Journey to PostScript