// ==UserScript== // @name GitHub Font Preview // @version 1.0.26 // @description A userscript that adds a font file preview // @license MIT // @author Rob Garrison // @namespace https://github.com/Mottie // @match https://github.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect github.com // @connect githubusercontent.com // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163 // @require https://greasyfork.org/scripts/20469-opentype-js/code/opentypejs.js?version=130870 // @icon https://github.githubassets.com/pinned-octocat.svg // @updateURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @supportURL https://github.com/Mottie/GitHub-userscripts/issues // ==/UserScript== /* global opentype */ (() => { "use strict"; let font; let showUnicode = GM_getValue("gfp-show-unicode", false); let showPoints = GM_getValue("gfp-show-points", true); let showArrows = GM_getValue("gfp-show-arrows", true); let currentIndex = 0; // supported font types const fontExt = /\.(otf|ttf|woff)$/i; // canvas colors const glyphFillColor = "#808080"; // (big) (mini) fill color const bigGlyphStrokeColor = "#111111"; // (big) stroke color const bigGlyphMarkerColor = "#f00"; // (big) min & max width marker const miniGlyphMarkerColor = "#606060"; // (mini) glyph index (bottom left corner) const glyphRulerColor = "#a0a0a0"; // (mini) min & max width marker & (big) glyph horizontal lines function startLoad() { const block = $(".blob-wrapper a[href*='?raw=true']"); const body = block && block.closest(".Box-body"); if (body) { body.classList.add("ghfp-body"); body.innerHTML = ""; } return block && block.href; } function getFont() { const url = startLoad(); if (url) { // add loading indicator GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", onload: response => { setupFont(response.response); } }); } } function setupFont(data) { const block = $(".ghfp-body"); const el = $(".final-path"); if (block && el) { try { font = opentype.parse(data); addHTML(block, el); showErrorMessage(""); onFontLoaded(font); } catch (err) { block.innerHTML = "
"; showErrorMessage(err.toString()); if (err.stack) { console.error(err.stack); } throw (err); } } } function addHTML(block, el) { let name = el.textContent || ""; block.innerHTML = `' + contour.map(point => {
return 'x=' + point.x + ' y=' + point.y + '';
}).join('\n') + '
';
}
function formatUnicode(unicode) {
unicode = unicode.toString(16);
if (unicode.length > 4) {
return ('000000' + unicode.toUpperCase()).substr(-6);
} else {
return ('0000' + unicode.toUpperCase()).substr(-4);
}
}
function displayGlyphData(glyphIndex) {
let glyph, contours, html,
container = document.getElementById('gfp-glyph-data'),
addItem = name => {
return glyph[name] ? `' + glyph.path.commands.map(pathCommandToString).join('\n ') + '\n'; } container.innerHTML = html; } function drawArrow(ctx, x1, y1, x2, y2) { let dx = x2 - x1, dy = y2 - y1, segmentLength = Math.sqrt(dx * dx + dy * dy), unitx = dx / segmentLength, unity = dy / segmentLength, basex = x2 - arrowLength * unitx, basey = y2 - arrowLength * unity, normalx = arrowAperture * unity, normaly = -arrowAperture * unitx; ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(basex + normalx, basey + normaly); ctx.lineTo(basex - normalx, basey - normaly); ctx.lineTo(x2, y2); ctx.closePath(); ctx.fill(); } /** * This function is Path.prototype.draw with an arrow * at the end of each contour. */ function drawPathWithArrows(ctx, path) { let indx, cmd, x1, y1, x2, y2, arrows = [], len = path.commands.length; ctx.beginPath(); for (indx = 0; indx < len; indx++) { cmd = path.commands[indx]; if (cmd.type === 'M') { if (x1 !== undefined) { arrows.push([ctx, x1, y1, x2, y2]); } ctx.moveTo(cmd.x, cmd.y); } else if (cmd.type === 'L') { ctx.lineTo(cmd.x, cmd.y); x1 = x2; y1 = y2; } else if (cmd.type === 'C') { ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); x1 = cmd.x2; y1 = cmd.y2; } else if (cmd.type === 'Q') { ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); x1 = cmd.x1; y1 = cmd.y1; } else if (cmd.type === 'Z') { arrows.push([ctx, x1, y1, x2, y2]); ctx.closePath(); } x2 = cmd.x; y2 = cmd.y; } if (path.fill) { ctx.fillStyle = path.fill; ctx.fill(); } if (path.stroke) { ctx.strokeStyle = path.stroke; ctx.lineWidth = path.strokeWidth; ctx.stroke(); } ctx.fillStyle = bigGlyphStrokeColor; if (showArrows) { arrows.forEach(arrow => { drawArrow.apply(null, arrow); }); } } function displayGlyph(glyphIndex) { let glyph, glyphWidth, xmin, xmax, x0, markSize, path, canvas = document.getElementById('gfp-glyph'), ctx = canvas.getContext('2d'), width = canvas.width / pixelRatio, height = canvas.height / pixelRatio; ctx.clearRect(0, 0, width, height); if (glyphIndex < 0) { return; } glyph = font.glyphs.get(glyphIndex); glyphWidth = glyph.advanceWidth * glyphScale; xmin = (width - glyphWidth) / 2; xmax = (width + glyphWidth) / 2; x0 = xmin; markSize = 10; ctx.fillStyle = bigGlyphMarkerColor; ctx.fillRect(xmin - markSize + 1, glyphBaseline, markSize, 1); ctx.fillRect(xmin, glyphBaseline, 1, markSize); ctx.fillRect(xmax, glyphBaseline, markSize, 1); ctx.fillRect(xmax, glyphBaseline, 1, markSize); ctx.textAlign = 'center'; ctx.fillText('0', xmin, glyphBaseline + markSize + 10); ctx.fillText(glyph.advanceWidth, xmax, glyphBaseline + markSize + 10); ctx.fillStyle = bigGlyphStrokeColor; path = glyph.getPath(x0, glyphBaseline, glyphSize); path.fill = glyphFillColor; path.stroke = bigGlyphStrokeColor; path.strokeWidth = 1.5; drawPathWithArrows(ctx, path); if (showPoints) { glyph.drawPoints(ctx, x0, glyphBaseline, glyphSize); } } function renderGlyphItem(canvas, glyphIndex) { const cellMarkSize = 4, ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, cellWidth, cellHeight); if (glyphIndex >= font.numGlyphs) { return; } ctx.fillStyle = miniGlyphMarkerColor; ctx.font = '10px sans-serif'; let glyph = font.glyphs.get(glyphIndex), glyphWidth = glyph.advanceWidth * fontScale, xmin = (cellWidth - glyphWidth) / 2, xmax = (cellWidth + glyphWidth) / 2, x0 = xmin; ctx.fillText(showUnicode ? glyph.unicodes.map(formatUnicode).join(', ') : glyphIndex, 1, cellHeight - 1); ctx.fillStyle = glyphRulerColor; ctx.fillRect(xmin - cellMarkSize + 1, fontBaseline, cellMarkSize, 1); ctx.fillRect(xmin, fontBaseline, 1, cellMarkSize); ctx.fillRect(xmax, fontBaseline, cellMarkSize, 1); ctx.fillRect(xmax, fontBaseline, 1, cellMarkSize); ctx.fillStyle = '#000000'; let path = glyph.getPath(x0, fontBaseline, fontSize); path.fill = glyphFillColor; path.draw(ctx); } function displayGlyphPage(pageNum) { pageSelected = pageNum; const last = $('.gfp-page-selected'); if (last) last.className = ''; document.getElementById('gfp-p' + pageNum).className = 'gfp-page-selected'; let indx, firstGlyph = pageNum * cellCount; for (indx = 0; indx < cellCount; indx++) { renderGlyphItem(document.getElementById('gfp-g' + indx), firstGlyph + indx); } } function pageSelect(event) { displayGlyphPage((event.target.id || '').replace('gfp-p', '')); } function initGlyphDisplay() { let glyphBgCanvas = document.getElementById('gfp-glyph-bg'), w = glyphBgCanvas.width / pixelRatio, h = glyphBgCanvas.height / pixelRatio, glyphW = w - glyphMargin * 2, glyphH = h - glyphMargin * 2, head = font.tables.head, maxHeight = head.yMax - head.yMin, ctx = glyphBgCanvas.getContext('2d'); glyphScale = Math.min(glyphW / (head.xMax - head.xMin), glyphH / maxHeight); glyphSize = glyphScale * font.unitsPerEm; glyphBaseline = glyphMargin + glyphH * head.yMax / maxHeight; function hline(text, yunits) { let ypx = glyphBaseline - yunits * glyphScale; ctx.fillText(text, 2, ypx + 3); ctx.fillRect(80, ypx, w, 1); } ctx.clearRect(0, 0, w, h); ctx.fillStyle = glyphRulerColor; hline('Baseline', 0); hline('yMax', font.tables.head.yMax); hline('yMin', font.tables.head.yMin); hline('Ascender', font.tables.hhea.ascender); hline('Descender', font.tables.hhea.descender); hline('Typo Ascender', font.tables.os2.sTypoAscender); hline('Typo Descender', font.tables.os2.sTypoDescender); } function onFontLoaded(font) { let indx, link, lastIndex, w = cellWidth - cellMarginLeftRight * 2, h = cellHeight - cellMarginTop - cellMarginBottom, head = font.tables.head, maxHeight = head.yMax - head.yMin, pagination = document.getElementById('gfp-pagination'), fragment = document.createDocumentFragment(), numPages = Math.ceil(font.numGlyphs / cellCount); fontScale = Math.min(w / (head.xMax - head.xMin), h / maxHeight); fontSize = fontScale * font.unitsPerEm; fontBaseline = cellMarginTop + h * head.yMax / maxHeight; pagination.innerHTML = ''; for (indx = 0; indx < numPages; indx++) { link = document.createElement('a'); lastIndex = Math.min(font.numGlyphs - 1, (indx + 1) * cellCount - 1); link.textContent = indx * cellCount + '-' + lastIndex; link.id = 'gfp-p' + indx; link.addEventListener('click', pageSelect, false); fragment.appendChild(link); // A white space allows to break very long lines into multiple lines. // This is needed for fonts with thousands of glyphs. fragment.appendChild(document.createTextNode(' ')); } pagination.appendChild(fragment); displayFontData(); initGlyphDisplay(); displayGlyphPage(0); displayGlyph(-1); displayGlyphData(-1); } function cellSelect(event) { if (!font) { return; } let firstGlyphIndex = pageSelected * cellCount, cellIndex = event ? +event.target.id.replace('gfp-g', '') : currentIndex, glyphIndex = firstGlyphIndex + cellIndex; currentIndex = cellIndex; if (glyphIndex < font.numGlyphs) { displayGlyph(glyphIndex); displayGlyphData(glyphIndex); } } function prepareGlyphList() { let indx, canvas, marker = document.getElementById('gfp-glyph-list-end'), parent = marker.parentElement; for (indx = 0; indx < cellCount; indx++) { canvas = document.createElement('canvas'); canvas.width = cellWidth; canvas.height = cellHeight; canvas.className = 'gfp-item ghd-invert'; canvas.id = 'gfp-g' + indx; canvas.addEventListener('click', cellSelect, false); enableHighDPICanvas(canvas); parent.insertBefore(canvas, marker); } } /* eslint-enable */ })();