No images in this repository’s iceberg at this time
Download raw (6.5 KB)
const fs = require('fs'); const { dialog } = require('electron').remote var font; class Font { constructor(xml) { this.xml = xml; let parser = new DOMParser(); this.dom = parser.parseFromString(xml, 'text/xml'); } table(name) { return this.dom.querySelector(name) }; glyphs() { return this.table('glyf').querySelectorAll('TTGlyph'); }; glyph(name) { return this.table('glyf').querySelector(`TTGlyph[name="${name}"]`); }; variations() { return this.table('fvar'); }; glyphVariations() { return this.table('gvar').querySelectorAll('glyphVariations'); }; glyphVariation(name) { return this.table('gvar').querySelector(`glyphVariations[glyph="${name}"]`); }; delta(name, point) { return this.glyphVariation(name).querySelectorAll(`delta[pt="${point}"]`); }; contours(name) { return this.glyph(name).querySelectorAll('contour'); }; } function deriveCurvePoint(p1, p2) { return { 'x': p1.x + .5 * (p2.x - p1.x), 'y': p1.y + .5 * (p2.y - p1.y), 'on': false, 'derived': true }; } function drawGlyph(name, font) { var canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); var glyph = font.glyph(name); var xmin = glyph.getAttribute('xMin'), xmax = glyph.getAttribute('xMax'), ymin = glyph.getAttribute('yMin'), ymax = glyph.getAttribute('yMax'), glyphWidth = xmax - xmin, glyphHeight = ymax - ymin; prepareCanvas = (ctx) => { ctx.transform(1, 0, 0, -1, 0, canvas.height); ctx.translate((canvas.width - glyphWidth) / 2 - xmin, (canvas.height - glyphHeight) / 2); }; ctx.save(); prepareCanvas(ctx); var glyphTTYPoints = []; font.contours(name).forEach(contour => { ctx.beginPath(); var contourTTYPoints = []; var curvepoints = []; contour.querySelectorAll('pt').forEach((point) => { var x = parseInt(point.getAttribute('x')), y = parseInt(point.getAttribute('y')), on = (point.getAttribute('on') === "1") ? true : false; contourTTYPoints.push({ 'x': x, 'y': y, 'on': on }); }); var prevPoint = contourTTYPoints[contourTTYPoints.length - 1]; for (var i = 0; i < contourTTYPoints.length; i++) { var point = contourTTYPoints[i]; if (!point.on) { if (!prevPoint.on) { curvepoints.push(deriveCurvePoint(point, prevPoint)); } else if (i === 0) { curvepoints.push(prevPoint); } } curvepoints.push(point); prevPoint = point; } if (!curvepoints[curvepoints.length - 1].on) { curvepoints.push(curvepoints[0]); } for (var i = 0; i < curvepoints.length; i++) { var p = curvepoints[i]; if (p.on) { if (i === 0) { ctx.moveTo(p.x, p.y); } else { ctx.lineTo(p.x, p.y); } } else { var cp = p, p = curvepoints[i + 1]; ctx.quadraticCurveTo(cp.x, cp.y, p.x, p.y); i++; } } ctx.closePath(); ctx.stroke(); glyphTTYPoints = glyphTTYPoints.concat(contourTTYPoints); }); // Mark points ctx.save(); ctx.strokeStyle = 'darkblue'; ctx.fillStyle = 'darkblue' glyphTTYPoints.forEach((p, k) => { ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI, false); if (p.on) { ctx.fill(); } else { ctx.stroke(); } ctx.save(); ctx.strokeStyle = 'orange'; ctx.fillStyle = 'orange'; font.delta(name, k).forEach((d) => { var x = p.x, y = p.y, dx = parseInt(d.getAttribute('x')), dy = parseInt(d.getAttribute('y')); if (dx !== 0 || dy !== 0) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + dx, y + dy); ctx.stroke(); arrowHead(ctx, x + dx, y + dy, calcAngle(dx, dy), 10); } ctx.beginPath(); ctx.arc(x + dx, y + dy, 2, 0, Math.PI * 2, false); ctx.fill(); }); ctx.restore(); }); ctx.restore(); ctx.restore(); } function fillGlyphSelector(selected) { var glyphSelector = document.getElementById('glyph-selector'); while (glyphSelector.firstChild) { glyphSelector.removeChild(glyphSelector.firstChild); } font.glyphs().forEach((glyph) => { var option = document.createElement('option'); option.value = glyph.getAttribute('name'); option.appendChild(document.createTextNode(option.value)); if (selected && option.value === selected) { option.selected = true; } glyphSelector.appendChild(option); }); } function arrowHead(ctx, x, y, angle, size) { ctx.save(); ctx.translate(x, y); // ctx.arc(0, 0, 10, 0, 2 * Math.PI, false); // // ctx.stroke(); ctx.rotate(angle + Math.PI * .75); ctx.scale(size / 10, size / 10); ctx.beginPath(); ctx.moveTo(3, 10); ctx.lineTo(0, 0); ctx.lineTo(10, 3); // ctx.endPath(); ctx.stroke(); ctx.restore(); } function calcAngle(dx, dy) { return Math.atan(dy / dx) + ((dx < 0) ? Math.PI : 0); } function loadTTX(path) { const xml = fs.readFileSync(path); font = new Font(xml); const selectedGlyph = localStorage.getItem('TTXInspectoreSelectedGlyph'); fillGlyphSelector(selectedGlyph); if (selectedGlyph) { drawGlyph(selectedGlyph, font); } } function loadFont() { var paths = dialog.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'ttx', extensions: ['ttx'] }] }); if (paths && paths.length > 0) { const path = paths[0]; localStorage.setItem('TTXInspectorFontPath', path); localStorage.removeItem('TTXInspectoreSelectedGlyph'); var canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); loadTTX(path); } } function loadFromStorage() { const path = localStorage.getItem('TTXInspectorFontPath') if (path) { loadTTX(path); } } document.getElementById('select-font').addEventListener('click', loadFont); document.getElementById('glyph-selector').addEventListener('change', function () { var value = this.value; localStorage.setItem('TTXInspectoreSelectedGlyph', value); drawGlyph(value, font); }); document.getElementById('export-canvas').addEventListener('click', () => { const canvas = document.getElementById('canvas'), path = dialog.showSaveDialog({}); reader = new FileReader(); reader.onload = function () { fs.writeFileSync(path, Buffer.from(this.result)); } if (path) { canvas.toBlob((blob) => { reader.readAsArrayBuffer(blob); }, 'image/png'); } }); document.getElementById('reload-font').addEventListener('click', loadFromStorage); loadFromStorage();