// Load, parse, and display SVG 1.1 Fonts, as specified in // https://www.w3.org/TR/SVG11/fonts.html // Ideal for single-stroke SVG Fonts, such as those at: // https://gitlab.com/oskay/svg-fonts // https://github.com/Shriinivas/inkscapestrokefont // https://github.com/isdat-type/Relief-SingleLine // p5 parser/displayer by Golan Levin, December 2024 let mySvgFont; function preload() { //// Here are some SVG fonts to try: // let fileIn = "single_line_svg_fonts/Hershey/HersheySans1.svg"; // let fileIn = "single_line_svg_fonts/Hershey/HersheyScript1.svg"; // let fileIn = "single_line_svg_fonts/EMS/EMSReadabilityItalic.svg"; // let fileIn = "single_line_svg_fonts/ISO3098/ISO3098-Regular.svg"; let fileIn = "single_line_svg_fonts/Relief/ReliefSingleLine-Regular.svg"; mySvgFont = new SvgFont(fileIn); } function setup() { createCanvas(800, 850); noLoop(); } function draw() { background("black"); stroke("white"); noFill(); let tracking = 0 let sca = 42; //42 let ty = 30; let dy = 50; //50 mySvgFont.drawString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 60, ty+=dy, sca, tracking); mySvgFont.drawString("abcdefghijklmnopqrstuvwxyz", 60, ty+=dy, sca, tracking); mySvgFont.drawString("1234567890", 60, ty+=dy, sca, tracking); mySvgFont.drawString("!@#$%^&*,.?/;:'-+_", 60,ty+=dy, sca, tracking); mySvgFont.drawString("()[]{}<>|\u00A9\u00AE\u20AC", 60, ty+=dy, sca, tracking); mySvgFont.drawString("Hello World!", 60, ty+=dy, sca, tracking); //--------------------------- // Plot extended ASCII characters ty+=dy*1.5; sca = 21; dy = 26; let decoder = new TextDecoder("windows-1252"); for (let i = 128; i <= 255; i++) { let hex = i.toString(16).toUpperCase().padStart(2, "0"); let bytes = new Uint8Array([i]); // Decode byte value i as Windows-1252 let ch = decoder.decode(bytes); // Don't use String.fromCharCode(i); let str = hex + " " + ch; let tx = 63+(i%8)*87; stroke(102); mySvgFont.drawString(hex, tx, ty, sca, tracking); stroke("white"); mySvgFont.drawString(ch, tx+36, ty, sca, tracking); if (i%8 == 7){ ty+=dy; } } } function keyPressed(){ if (key == 's'){ save("p5_single_line_svg_font.png"); } } //===================================================== // 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"); // Read font-level default advance width (individual glyphs may omit horiz-adv-x) const fontEl = svgDoc.querySelector("font"); this.defaultHorizAdvX = parseFloat(fontEl?.getAttribute("horiz-adv-x") || 0); // 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") ?? this.defaultHorizAdvX); this.glyphs[unicode] = { d: pathData, horizAdvX }; } }); // Ensure a space glyph exists — XML parsers may drop whitespace-only attribute values if (!this.glyphs[' ']) { this.glyphs[' '] = { d: null, horizAdvX: this.defaultHorizAdvX }; } // 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 1) { const s = Math.sqrt(lambda); rx *= s; ry *= s; rx2 = rx*rx; ry2 = ry*ry; } // Center in rotated frame const num = rx2*ry2 - rx2*y1p2 - ry2*x1p2; const den = rx2*y1p2 + ry2*x1p2; const sign = (fA === fS) ? -1 : 1; const coef = sign * Math.sqrt(Math.max(0, num / den)); const cxp = coef * rx * y1p / ry; const cyp = -coef * ry * x1p / rx; // Center in original frame const cx = cosPhi*cxp - sinPhi*cyp + (x1+x2)/2; const cy = sinPhi*cxp + cosPhi*cyp + (y1+y2)/2; // Start angle and sweep const ux = (x1p - cxp) / rx, uy = (y1p - cyp) / ry; const vx = (-x1p - cxp) / rx, vy = (-y1p - cyp) / ry; const theta1 = _svgArcAngle(1, 0, ux, uy); let dTheta = _svgArcAngle(ux, uy, vx, vy); if (!fS && dTheta > 0) dTheta -= 2 * Math.PI; if ( fS && dTheta < 0) dTheta += 2 * Math.PI; // Split into ≤90° segments and convert each to cubic Bézier const nSegs = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2))); const dSeg = dTheta / nSegs; const alpha = Math.sin(dSeg) * (Math.sqrt(4 + 3 * Math.tan(dSeg/2) ** 2) - 1) / 3; const curves = []; let curAngle = theta1, curX = x1, curY = y1; for (let i = 0; i < nSegs; i++) { const nextAngle = curAngle + dSeg; const cosA2 = Math.cos(nextAngle), sinA2 = Math.sin(nextAngle); const ex = cx + cosPhi*rx*cosA2 - sinPhi*ry*sinA2; const ey = cy + sinPhi*rx*cosA2 + cosPhi*ry*sinA2; // Tangent (dP/dθ) at start and end of this segment const cosA1 = Math.cos(curAngle), sinA1 = Math.sin(curAngle); const dx1t = -(cosPhi*rx*sinA1 + sinPhi*ry*cosA1); const dy1t = -(sinPhi*rx*sinA1 - cosPhi*ry*cosA1); const dx2t = -(cosPhi*rx*sinA2 + sinPhi*ry*cosA2); const dy2t = -(sinPhi*rx*sinA2 - cosPhi*ry*cosA2); curves.push([ curX, curY, curX + alpha*dx1t, curY + alpha*dy1t, ex - alpha*dx2t, ey - alpha*dy2t, ex, ey, ]); curAngle = nextAngle; curX = ex; curY = ey; } return curves; } //--------------------------------------------------------- // Draw a string of text using the parsed font. // tracking: extra pixels added after each glyph (negative tightens spacing) drawString(str, x, y, sca, tracking = 0) { if (this.isReady()) { let cursorX = x; const scaleFactor = sca / this.unitsPerEm; for (const chr of str) { const glyph = this.glyphs[chr]; if (glyph) { // Only draw if there's path data if (glyph.d) { this.drawGlyph(glyph.d, cursorX, y, sca); } // Always advance cursorX using horiz-adv-x cursorX += glyph.horizAdvX * scaleFactor + tracking; } else { // console.warn(`Missing glyph: '${chr}' (Unicode: ${chr.charCodeAt(0)})`); cursorX += 300 * scaleFactor + tracking; // Fallback spacing for missing glyphs } } } } } // Signed angle from vector (ux,uy) to (vx,vy), in radians. // Used by SvgFont._arcToBeziers for SVG arc parameterization. function _svgArcAngle(ux, uy, vx, vy) { const dot = ux*vx + uy*vy; const len = Math.sqrt((ux*ux + uy*uy) * (vx*vx + vy*vy)); const angle = Math.acos(Math.max(-1, Math.min(1, dot / len))); return (ux*vy - uy*vx < 0) ? -angle : angle; }