Source code of plot #060 back to plot

Download full working sketch as 060.tar.gz.
Unzip, then start a local web server and load the page in a browser.

class Glyph {
  constructor(adv, paths) {
    this.adv = adv;
    this.paths = paths;
  }
}

/**
 * Returned as a rendered text. Contains an SVG group, and the width of the rendered text.
 */
export class TextResult {
  /**
   * @param {SVGGElement} g - An SVG group element.
   * @param {number} width - Width of the rendered text.
   */
  constructor(g, width) {
    this.g = g;
    this.width = width;
  }
}

export class SvgFont {
  constructor(xmlStr) {
    this.strokeWidthPx = 2;
    const doc = new DOMParser().parseFromString(xmlStr, "text/xml");
    const elmFace = doc.getElementsByTagName("font-face")[0];
    this.ascent = Number(elmFace.getAttribute("ascent"));
    this.descent = Number(elmFace.getAttribute("descent"));
    this.capHeight = Number(elmFace.getAttribute("cap-height"));
    this.noVerticalFlip = elmFace.getAttribute("no-vertical-flip") == "true";
    this.glyphs = {};
    const elmGlyphs = doc.getElementsByTagName("glyph");
    for (let i = 0; i < elmGlyphs.length; ++i) {
      const elmGlyph = elmGlyphs[i];
      const c = elmGlyph.getAttribute("unicode");
      const adv = Number(elmGlyph.getAttribute("horiz-adv-x"));
      let path = elmGlyph.getAttribute("d");
      let paths = [];
      if (path) {
        let pathParts = path.split("M");
        for (let i = 0; i < pathParts.length; ++i) {
          let pathPart = pathParts[i].trim();
          if (pathPart == "") continue;
          paths.push("M" + pathPart);
        }
      }
      const glyph = new Glyph(adv, paths);
      this.glyphs[c] = glyph;
    }
  }

  /**
   * Renders text to an SVG group element.
   * @param {number} size Font size.
   * @param {string} text The text to write.
   * @returns {TextResult} Rendered result.
   */
  write(size, text) {
    let scaleVal = size / (this.ascent - this.descent);
    const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
    let x = 0;
    for (const c of text) {
      const glyph = this.glyphs.hasOwnProperty(c) ? this.glyphs[c] : this.glyphs["*"];
      for (const pathStr of glyph.paths) {
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", pathStr);
        path.setAttribute("stroke", "black");
        path.setAttribute("stroke-width", `${this.strokeWidthPx/scaleVal}px`);
        path.setAttribute("fill", "none");
        path.setAttribute("transform", `translate(${x}, ${this.descent})`);
        g.appendChild(path);
      }
      x += glyph.adv;
    }
    let scaleX = scaleVal;
    let scaleY = this.noVerticalFlip ? scaleVal : -scaleVal;
    g.setAttribute("transform", `translate(0, ${this.descent*scaleVal}) scale(${scaleX}, ${scaleY})`);
    const outerG = document.createElementNS("http://www.w3.org/2000/svg", "g");
    outerG.appendChild(g);
    return new TextResult(outerG, x * scaleVal);
  }

  // writeOnArc(size, baselineRadius, text, bottom, segLen = 2) {
  //
  //   let distort = (pt, adv, angleAdv) => {
  //     let rad = baselineRadius + pt.y;
  //     let angle = angleAdv * pt.x / adv;
  //     return new Point(0, rad).rotate(-angle).add(0, -baselineRadius);
  //   };
  //
  //   const origin = new Point(0, 0);
  //   const gText = new paper.Group();
  //   const scale = size / (this.ascent - this.descent);
  //   let angle = bottom ? 180 : 0;
  //   let spoke = new Point(0, -baselineRadius);
  //   if (!bottom) spoke.y -= this.capHeight * scale;
  //   let vflip = this.noVerticalFlip && !bottom || !this.noVerticalFlip && bottom;
  //   for (const c of text) {
  //     const glyph = this.glyphs.hasOwnProperty(c) ? this.glyphs[c] : this.glyphs["*"];
  //     let angleAdv = (glyph.adv * scale / baselineRadius) * 180 / Math.PI;
  //     if (!bottom) angleAdv = -angleAdv;
  //     let gGlyph = new paper.Group();
  //     for (const pathStr of glyph.paths) {
  //       const path = new paper.Path(pathStr);
  //       if (vflip) path.scale(1, -1, origin);
  //       else path.translate(0, -this.capHeight);
  //       path.scale(scale, origin);
  //       let pts = [];
  //       for (let i = 0; i < path.segments.length - 1; ++i) {
  //         let seg1 = path.segments[i];
  //         let seg2 = path.segments[i+1];
  //         pts.push(...getLinePts(seg1.point, seg2.point, segLen));
  //         if (i < path.segments.length - 2) pts.pop();
  //       }
  //       if (path.closed) pts.push(path.segments[0].point);
  //       for (let i = 0; i < pts.length; ++i) pts[i] = distort(pts[i], glyph.adv * scale, angleAdv);
  //       gGlyph.addChild(new Path({segments: pts}));
  //     }
  //     gGlyph.rotate(angle - 180, origin);
  //     gGlyph.translate(spoke.rotate(angle, origin));
  //     gText.addChild(gGlyph);
  //     angle -= angleAdv;
  //   }
  //   if (bottom) return [gText, 180 - angle];
  //   else return [gText, angle];
  // }
}

// function getLinePts(pt1, pt2, segLen) {
//   const lineVect = pt2.subtract(pt1);
//   const lineLength = lineVect.length;
//   const nSegs = Math.max(2, Math.round(lineLength / segLen));
//   const segVect = lineVect.divide(nSegs);
//   const pts = [];
//   for (let i = 0; i <= nSegs; ++i) {
//     pts.push(pt1.add(segVect.multiply(i)));
//   }
//   return pts;
// }