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.
Run 10 dict begin /transparent 1 def /rawurl 1 def currentdict setpagedevice end
0.2 0.2 0.9 setrgbcolor
0.5 setalpha
20 setlinewidth
100 100 moveto 200 100 lineto 200 200 lineto 100 200 lineto closepath stroke
250 100 moveto 350 100 lineto 300 200 lineto closepath stroke
450 100 moveto
480 100 500 120 500 150 curveto
500 180 480 200 450 200 curveto
420 200 400 180 400 150 curveto
400 120 420 100 450 100 curveto stroke
showpage
PNG raw PNG canvas SVG PDF
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