I want to extract the centerline of a character, through client-side JavaScript, from .ttf (TrueType Fonts). TrueType fonts store characters as 2 paths, an inner and outer paths. However, I need the centerline of the character (refer image, I am trying to achieve something similar to this.)
How would I go about implementing this?
I am already able to extract the outer and inner path from fonts using opentype.js
I'm afraid you're out of luck.
The main problem is we can't really interpolate a center-line from outer path-geometry.
An alternative approach is often referred to as "skeleton-tracing" which analyzes a bitmap to find inner skeleton structures.
Unfortunately this approach is not well suited for letter-shapes as it introduces rather jittery "bones".
Besides, the algorithm won't auto-detect a reasonable drawing order as expected from the usual writing conventions.
Here's an example I wrote when experimenting with this approach. I'm using LingDong's skeleton-tracing JS script.
let svg = document.querySelector('svg')
// add scaling to improve accuracy
let scale = 2
svg2Skeleton(svg, scale);
/**
* svg to canvas
*/
async function svg2Skeleton(el, scale = 1, filter = "") {
/**
* clone svg to add width and height
* for better compatibility
* without affecting the original svg
*/
const svgEl = el.cloneNode(true);
// get dimensions
let {
width,
height
} = el.getBBox();
let w = el.viewBox.baseVal.width ?
svgEl.viewBox.baseVal.width :
el.width.baseVal.value ?
el.width.baseVal.value :
width;
let h = el.viewBox.baseVal.height ?
svgEl.viewBox.baseVal.height :
el.height.baseVal.value ?
el.height.baseVal.value :
height;
// apply scaling
[w, h] = [w * scale, h * scale];
// add width and height for firefox compatibility
svgEl.setAttribute("width", w);
svgEl.setAttribute("height", h);
// create canvas
let canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
// create blob
let svgString = new XMLSerializer().serializeToString(svgEl);
let blob = new Blob([svgString], {
type: "image/svg+xml"
});
let objectURL = URL.createObjectURL(blob);
let tmpImg = new Image();
tmpImg.src = objectURL;
tmpImg.width = w;
tmpImg.height = h;
tmpImg.crossOrigin = "anonymous";
await tmpImg.decode();
let ctx = canvas.getContext("2d");
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
// apply filter to enhance contrast
filter = 'brightness(0) grayscale(1) invert(1)';
if (filter) {
ctx.filter = filter;
}
ctx.drawImage(tmpImg, 0, 0, w, h);
// get skeleton
let {
polylines
} = await TraceSkeleton.fromCanvas(canvas)
let polyArr = polylines.map(poly => poly.flat());
// render paths
let paths = '';
polyArr.forEach(poly => {
let path = `<path d="M ${poly.map(val => val / scale).join(' ')}" />`;
paths += path;
})
svg.insertAdjacentHTML('beforeend', paths);
}
<script src="https://cdn.jsdelivr.net/npm/skeleton-tracing-js/dist/trace_skeleton.min.js"></script>
<svg viewBox="0 0 500 500">
<style>
path {
fill: none;
stroke: red;
stroke-width: 1px !important
}
path:nth-of-type(even) {
fill: none;
stroke: green;
stroke-width: 1px !important;
}
</style>
<text x="10" y="200" class="text" font-family="arial" font-size="160" stroke="#fff" stroke-width="0">AE</text>
</svg>
We're rendering the SVG to a canvas and apply the tracing to the bitmap data.
As you can see the result is rather underwhelming. Admittedly you could apply some sort of polygon simplification to get smoother skeleton lines.
All in all you will get a way better result drawing the center-cines manually in a graphic editor.
See also K. Palágyi's article