// ==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 */
})();