Created
August 8, 2023 07:07
-
-
Save folknor/da97a716bd77b1d2f12f404e19601e10 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2016 Hideki Shiro | |
* Manually adapted for private project use by https://github.com/folknor because | |
* the original repository hasn't been updated in a while and there are | |
* pending pull requests and other issues that I needed. | |
*/ | |
import opentype from "opentype.js" | |
// Private method | |
function parseAnchorOption(anchor) { | |
let horizontal = anchor.match(/left|center|right/gi) || [] | |
horizontal = horizontal.length === 0 ? "left" : horizontal[0] | |
let vertical = anchor.match(/baseline|top|bottom|middle/gi) || [] | |
vertical = vertical.length === 0 ? "baseline" : vertical[0] | |
return { horizontal, vertical } | |
} | |
class TextToSVG { | |
constructor(font) { | |
this.font = font | |
} | |
static load(url, cb) { | |
opentype.load(url, (err, font) => { | |
if (err !== null) { | |
return cb(err, null) | |
} | |
return cb(null, new TextToSVG(font)) | |
}) | |
} | |
getWidth(text, options) { | |
const fontSize = options.fontSize || 72 | |
const kerning = "kerning" in options ? options.kerning : true | |
const fontScale = (1 / this.font.unitsPerEm) * fontSize | |
let width = 0 | |
const glyphs = this.font.stringToGlyphs(text) | |
for (let i = 0; i < glyphs.length; i++) { | |
const glyph = glyphs[i] | |
if (glyph.advanceWidth) { | |
width += glyph.advanceWidth * fontScale | |
} | |
if (kerning && i < glyphs.length - 1) { | |
const kerningValue = this.font.getKerningValue(glyph, glyphs[i + 1]) | |
width += kerningValue * fontScale | |
} | |
if (options.letterSpacing) { | |
width += options.letterSpacing * fontSize | |
} else if (options.tracking) { | |
width += (options.tracking / 1000) * fontSize | |
} | |
} | |
return width | |
} | |
getHeight(fontSize) { | |
const fontScale = (1 / this.font.unitsPerEm) * fontSize | |
return (this.font.ascender - this.font.descender) * fontScale | |
} | |
getMetrics(text, options = {}) { | |
const fontSize = options.fontSize || 72 | |
const anchor = parseAnchorOption(options.anchor || "") | |
const width = this.getWidth(text, options) | |
const height = this.getHeight(fontSize) | |
const fontScale = (1 / this.font.unitsPerEm) * fontSize | |
const ascender = this.font.ascender * fontScale | |
const descender = this.font.descender * fontScale | |
let x = options.x || 0 | |
switch (anchor.horizontal) { | |
case "left": | |
x -= 0 | |
break | |
case "center": | |
x -= width / 2 | |
break | |
case "right": | |
x -= width | |
break | |
default: | |
throw new Error(`Unknown anchor option: ${anchor.horizontal}`) | |
} | |
let y = options.y || 0 | |
switch (anchor.vertical) { | |
case "baseline": | |
y -= ascender | |
break | |
case "top": | |
y -= 0 | |
break | |
case "middle": | |
y -= height / 2 | |
break | |
case "bottom": | |
y -= height | |
break | |
default: | |
throw new Error(`Unknown anchor option: ${anchor.vertical}`) | |
} | |
const baseline = y + ascender | |
return { | |
x, | |
y, | |
baseline, | |
width, | |
height, | |
ascender, | |
descender | |
} | |
} | |
getD(text, options = {}) { | |
return this.getDAndMetrics(text, options).d | |
} | |
getDAndMetrics(text, options = {}) { | |
const kerning = "kerning" in options ? options.kerning : true | |
const letterSpacing = "letterSpacing" in options ? options.letterSpacing : false | |
const tracking = "tracking" in options ? options.tracking : false | |
const metrics = this.getMetrics(text, options) | |
const path = this.font.getPath(text, metrics.x, metrics.baseline, options.fontSize || 72, Object.assign({}, options, { kerning, letterSpacing, tracking })) | |
const d = path.toPathData() | |
return { d, metrics } | |
} | |
getPath(text, options) { | |
return this.getPathAndMetrics(text, options).path | |
} | |
getPathAndMetrics(text, options = {}) { | |
const attributes = Object.keys(options.attributes || {}) | |
.map(key => `${key}="${options.attributes[key]}"`) | |
.join(" ") | |
const { d, metrics } = this.getDAndMetrics(text, options) | |
let path | |
if (attributes) { | |
path = `<path ${attributes} d="${d}"/>` | |
} else { | |
path = `<path d="${d}"/>` | |
} | |
return { path, metrics } | |
} | |
getSVG(text, options) { | |
return this.getSVGAndMetrics(text, options).svg | |
} | |
getSVGAndMetrics(text, options) { | |
const { path, metrics } = this.getPathAndMetrics(text, options) | |
let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${metrics.width.toFixed(2)} ${metrics.height.toFixed(2)}">` | |
svg += path | |
svg += "</svg>" | |
return { svg, metrics } | |
} | |
getDebugSVG(text, options = {}) { | |
options = JSON.parse(JSON.stringify(options)) | |
options.x = options.x || 0 | |
options.y = options.y || 0 | |
const { path, metrics } = this.getPathAndMetrics(text, options) | |
const box = { | |
width: Math.max(metrics.x + metrics.width, 0) - Math.min(metrics.x, 0), | |
height: Math.max(metrics.y + metrics.height, 0) - Math.min(metrics.y, 0) | |
} | |
const origin = { | |
x: box.width - Math.max(metrics.x + metrics.width, 0), | |
y: box.height - Math.max(metrics.y + metrics.height, 0) | |
} | |
// Shift text based on origin | |
options.x += origin.x | |
options.y += origin.y | |
let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 "${metrics.width}" "${metrics.height}">` | |
svg += `<path fill="none" stroke="red" stroke-width="1" d="M0,${origin.y}L${box.width},${origin.y}"/>` // X Axis | |
svg += `<path fill="none" stroke="red" stroke-width="1" d="M${origin.x},0L${origin.x},${box.height}"/>` // Y Axis | |
svg += path | |
svg += "</svg>" | |
return svg | |
} | |
} | |
export default TextToSVG |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment