p5.plotsvg
Version:
A Plotter-Oriented SVG Exporter for p5.js
287 lines (253 loc) • 9.86 kB
JavaScript
// Load, display, and export SVG designs with SVG 1.1 Fonts,
// as specified in https://www.w3.org/TR/SVG11/fonts.html
p5.disableFriendlyErrors = true; // hush, p5
let bDoExportSvg = false;
let mySvgFont;
function preload() {
//// Here are some SVG fonts to try:
mySvgFont = new SvgFont("HersheyScript1.svg");
// mySvgFont = new SvgFont("HersheySans1.svg");
// Lots more SVG single-line fonts can be found at:
// https://github.com/golanlevin/p5-single-line-font-resources/tree/main/p5_single_line_svg_fonts
// https://gitlab.com/oskay/svg-fonts
// https://github.com/isdat-type/Relief-SingleLine
}
function setup() {
createCanvas(640, 320);
let saveButton = createButton("Save SVG");
saveButton.position(10, 10);
saveButton.mousePressed((event) => {
event.stopPropagation();
bDoExportSvg = true;
});
}
function draw() {
background(245);
stroke(0);
if (bDoExportSvg){
beginRecordSVG(this, "svg_font_text.svg");
}
let sca = 30;
mySvgFont.drawString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 40, 80, sca);
mySvgFont.drawString("abcdefghijklmnopqrstuvwxyz", 40, 120, sca);
mySvgFont.drawString("1234567890", 40, 160, sca);
mySvgFont.drawString("!@#$%^&*,.?/;:'-+_", 40, 200, sca);
mySvgFont.drawString("()[]{}<>|\u00A9\u00AE\u20AC", 40, 240, sca);
mySvgFont.drawString("Hello World!", 40, 280, sca);
if (bDoExportSvg){
endRecordSVG();
bDoExportSvg = false;
}
}
function keyPressed() {
if ((key === 's') || (key === 'S')) {
bDoExportSvg = true;
}
}
//=====================================================
//=====================================================
// Class to handle SVG font parsing and rendering
class SvgFont {
constructor(filePath) {
this.glyphs = {};
this.unitsPerEm = 1000;
this.ready = false;
// Load the SVG font file
loadStrings(filePath, (strings) => {
this.loadData(strings.join("\n"));
this.ready = true;
});
}
isReady() {
return this.ready;
}
//---------------------------------------------------------
// Load and parse the SVG font data
loadData(svgData) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgData, "text/xml");
// Parse the glyphs
const glyphElements = svgDoc.querySelectorAll("glyph");
glyphElements.forEach((glyph) => {
const unicode = glyph.getAttribute("unicode");
if (unicode !== null) { // Ensure glyph has a valid unicode attribute
const pathData = glyph.getAttribute("d");
const horizAdvX = parseFloat(glyph.getAttribute("horiz-adv-x") || 0);
this.glyphs[unicode] = { d: pathData, horizAdvX };
}
});
// Parse font-face for scale metrics
const fontFace = svgDoc.querySelector("font-face");
if (fontFace) {
this.unitsPerEm = parseFloat(fontFace.getAttribute("units-per-em") || 1000);
}
}
//---------------------------------------------------------
// Draw a single glyph at the specified position and scale
drawGlyph(pathData, x, y, sca) {
const commands = pathData.match(/[A-Za-z][^A-Za-z]*/g) || [];
const nCommands = commands.length;
let currentX = x;
let currentY = y;
let prevControlX = null;
let prevControlY = null;
for (let i=0; i<nCommands; i++){
const command = commands[i];
const type = command[0];
const args = command
.slice(1)
.trim()
.split(/[ ,]+/)
.map(parseFloat);
let px, py;
switch (type) {
case "M": // Move to (absolute)
currentX = x + sca * (args[0] / this.unitsPerEm);
currentY = y - sca * (args[1] / this.unitsPerEm);
prevControlX = null;
prevControlY = null;
break;
case "m": // Move to (relative)
currentX += sca * (args[0] / this.unitsPerEm);
currentY -= sca * (args[1] / this.unitsPerEm);
prevControlX = null;
prevControlY = null;
break;
case "L": // Line to (absolute)
px = x + sca * (args[0] / this.unitsPerEm);
py = y - sca * (args[1] / this.unitsPerEm);
line(currentX, currentY, px, py);
currentX = px;
currentY = py;
prevControlX = null;
prevControlY = null;
break;
case "l": // Line to (relative)
px = currentX + sca * (args[0] / this.unitsPerEm);
py = currentY - sca * (args[1] / this.unitsPerEm);
line(currentX, currentY, px, py);
currentX = px;
currentY = py;
prevControlX = null;
prevControlY = null;
break;
case "H": // Horizontal line to (absolute)
px = x + sca * (args[0] / this.unitsPerEm);
line(currentX, currentY, px, currentY);
currentX = px;
prevControlX = null;
prevControlY = null;
break;
case "h": // Horizontal line to (relative)
px = currentX + sca * (args[0] / this.unitsPerEm);
line(currentX, currentY, px, currentY);
currentX = px;
prevControlX = null;
prevControlY = null;
break;
case "V": // Vertical line to (absolute)
py = y - sca * (args[0] / this.unitsPerEm);
line(currentX, currentY, currentX, py);
currentY = py;
prevControlX = null;
prevControlY = null;
break;
case "v": // Vertical line to (relative)
py = currentY - sca * (args[0] / this.unitsPerEm);
line(currentX, currentY, currentX, py);
currentY = py;
prevControlX = null;
prevControlY = null;
break;
case "C": // Cubic Bézier curve (absolute)
const x1 = currentX;
const y1 = currentY;
const x2 = x + sca * (args[0] / this.unitsPerEm);
const y2 = y - sca * (args[1] / this.unitsPerEm);
const x3 = x + sca * (args[2] / this.unitsPerEm);
const y3 = y - sca * (args[3] / this.unitsPerEm);
const x4 = x + sca * (args[4] / this.unitsPerEm);
const y4 = y - sca * (args[5] / this.unitsPerEm);
bezier(x1, y1, x2, y2, x3, y3, x4, y4);
currentX = x4;
currentY = y4;
prevControlX = x3;
prevControlY = y3;
break;
case "c": // Cubic Bézier curve (relative)
const relX1 = currentX;
const relY1 = currentY;
const relX2 = currentX + sca * (args[0] / this.unitsPerEm);
const relY2 = currentY - sca * (args[1] / this.unitsPerEm);
const relX3 = currentX + sca * (args[2] / this.unitsPerEm);
const relY3 = currentY - sca * (args[3] / this.unitsPerEm);
const relX4 = currentX + sca * (args[4] / this.unitsPerEm);
const relY4 = currentY - sca * (args[5] / this.unitsPerEm);
bezier(relX1, relY1, relX2, relY2, relX3, relY3, relX4, relY4);
currentX = relX4;
currentY = relY4;
prevControlX = relX3;
prevControlY = relY3;
break;
case "S": // Smooth cubic Bézier curve (absolute)
const smoothX2 = prevControlX ? 2 * currentX - prevControlX : currentX;
const smoothY2 = prevControlY ? 2 * currentY - prevControlY : currentY;
const smoothX3 = x + sca * (args[0] / this.unitsPerEm);
const smoothY3 = y - sca * (args[1] / this.unitsPerEm);
const smoothX4 = x + sca * (args[2] / this.unitsPerEm);
const smoothY4 = y - sca * (args[3] / this.unitsPerEm);
bezier(currentX, currentY, smoothX2, smoothY2,
smoothX3, smoothY3, smoothX4, smoothY4);
currentX = smoothX4;
currentY = smoothY4;
prevControlX = smoothX3;
prevControlY = smoothY3;
break;
case "s": // Smooth cubic Bézier curve (relative)
const relSmoothX2 = prevControlX ? 2 * currentX - prevControlX : currentX;
const relSmoothY2 = prevControlY ? 2 * currentY - prevControlY : currentY;
const relSmoothX3 = currentX + sca * (args[0] / this.unitsPerEm);
const relSmoothY3 = currentY - sca * (args[1] / this.unitsPerEm);
const relSmoothX4 = currentX + sca * (args[2] / this.unitsPerEm);
const relSmoothY4 = currentY - sca * (args[3] / this.unitsPerEm);
bezier(currentX, currentY, relSmoothX2, relSmoothY2,
relSmoothX3, relSmoothY3, relSmoothX4, relSmoothY4);
currentX = relSmoothX4;
currentY = relSmoothY4;
prevControlX = relSmoothX3;
prevControlY = relSmoothY3;
break;
default:
// console.warn(`Unsupported SVG command: ${type}`);
break;
}
}
}
//---------------------------------------------------------
// Draw a string of text using the parsed font.
// Modified to add SVG groups for each glyph,
// using p5.plotSvg's beginSvgGroup and endSvgGroup.
drawString(str, x, y, sca) {
if (this.isReady()) {
let cursorX = x;
const scaleFactor = sca / this.unitsPerEm;
noFill();
for (const chr of str) {
const glyph = this.glyphs[chr];
if (glyph) {
// Only draw if there's path data
if (glyph.d) {
beginSvgGroup();
this.drawGlyph(glyph.d, cursorX, y, sca);
endSvgGroup();
}
// Always advance cursorX using horiz-adv-x
cursorX += glyph.horizAdvX * scaleFactor;
} else {
console.warn(`Missing glyph: '${chr}' (Unicode: ${chr.charCodeAt(0)})`);
cursorX += 300 * scaleFactor; // Fallback spacing for missing glyphs
}
}
}
}
}