// constants var kSVGScale = 0.1 // how bmuch metrics are scaled in the SVGs var kGlyphSize = 346 // at kSVGScale. In sync with CSS and SVGs var kUPM = 2816 function pxround(n) { return Math.round(n * 2) / 2 } // forEachElement(selector, fun) // forEachElement(parent, selector, fun) function eachElement(parent, selector, fun) { if (typeof selector == 'function') { fun = selector selector = parent parent = document } Array.prototype.forEach.call(parent.querySelectorAll(selector), fun) } if (!isMac) { eachElement('kbd', function(e) { if (e.innerText == '\u2318') { e.innerText = 'Ctrl' } }) } function httpget(url, cb) { var req = new XMLHttpRequest(); req.addEventListener("load", function() { cb(this.responseText, this) }) req.open("GET", url) req.send() } function fetchGlyphInfo(cb) { httpget("../lab/glyphinfo.json", function(res) { cb(JSON.parse(res).glyphs) }) } function fetchMetrics(cb) { httpget("metrics.json", function(res) { cb(JSON.parse(res)) }) } var glyphInfo = null var glyphMetrics = null function initMetrics(data) { // metrics.json uses a compact format with numerical glyph indentifiers // in order to minimize file size. // We expand the glyph IDs to glyph names here. var nameIds = data.nameids // console.log(data) var metrics = {} var metrics0 = data.metrics Object.keys(metrics0).forEach(function (id) { var name = nameIds[id] var v = metrics0[id] // v : [width, advance, left, right] metrics[name] = { width: v[0], advance: v[1], left: v[2], right: v[3] } }) var kerningLeft = {} // { glyphName: {rightGlyphName: kerningValue, ...}, ... } var kerningRight = {} data.kerning.forEach(function (t) { // each entry looks like this: // [leftGlyphId, rightGlyphId, kerningValue] var leftName = nameIds[t[0]] if (!leftName) { console.error('nameIds missing', t[0]) } var rightName = nameIds[t[1]] if (!rightName) { console.error('nameIds missing', t[1]) } var kerningValue = t[2] var lm = kerningLeft[leftName] if (!lm) { kerningLeft[leftName] = lm = {} } lm[rightName] = kerningValue var rm = kerningRight[rightName] if (!rm) { kerningRight[rightName] = rm = {} } rm[leftName] = kerningValue }) glyphMetrics = { metrics: metrics, kerningLeft: kerningLeft, kerningRight: kerningRight, } // console.log('glyphMetrics', glyphMetrics) } function fetchAll(cb) { var i = 0 var res = {} var decr = function(){ if (--i == 0) { cb(res) } } i++ fetchGlyphInfo(function(r){ glyphInfo = r; decr() }) i++ fetchMetrics(function(r){ initMetrics(r) decr() }) } fetchAll(render) var styleSheet = document.styleSheets[document.styleSheets.length-1] var glyphRule, baselineRule, zeroWidthAdvRule var currentScale = 0 var defaultSingleScale = 1 var currentSingleScale = 1 var defaultGridScale = 0.4 var currentGridScale = defaultGridScale var glyphs = document.getElementById('glyphs') function updateLayoutAfterChanges() { var height = parseInt(window.getComputedStyle(glyphs).height) var margin = height - (height * currentScale) // console.log('scale:', currentScale, 'height:', height, 'margin:', margin) if (currentScale > 1) { glyphs.style.marginBottom = null } else { glyphs.style.marginBottom = -margin + 'px' } } function setScale(scale) { if (queryString.iframe !== undefined) { scale = 0.11 } else if (queryString.g) { scale = Math.min(Math.max(1, scale), 3) } else { scale = Math.min(Math.max(0.05, scale), 3) } if (currentScale == scale) { return } currentScale = scale if (queryString.g) { currentSingleScale = scale } else { currentGridScale = scale } var hairline = Math.ceil(1 / window.devicePixelRatio / scale) var spacing = Math.ceil(6 / scale) var s = glyphs.style if (queryString.g || queryString.iframe !== undefined) { s.paddingLeft = null } else { s.paddingLeft = spacing + 'px' } s.width = (100 / scale) + '%' s.transform = 'scale(' + scale + ')' if (!glyphRule) { glyphRule = styleSheet.cssRules[styleSheet.insertRule('#glyphs .glyph {}', styleSheet.cssRules.length)] baselineRule = styleSheet.cssRules[styleSheet.insertRule('#glyphs .glyph .baseline {}', styleSheet.cssRules.length)] zeroWidthAdvRule = styleSheet.cssRules[styleSheet.insertRule('#glyphs .glyph.zero-width .advance {}', styleSheet.cssRules.length)] } if (queryString.g) { glyphRule.style.marginRight = null glyphRule.style.marginBottom = null } else { glyphRule.style.marginRight = Math.ceil(6 / scale) + 'px'; glyphRule.style.marginBottom = Math.ceil(6 / scale) + 'px'; if (queryString.iframe !== undefined) { glyphRule.style.marginBottom = Math.ceil(16 / scale) + 'px'; } } baselineRule.style.height = hairline + 'px' zeroWidthAdvRule.style.borderWidth = (hairline) + 'px' updateLayoutAfterChanges() requestAnimationFrame(updateLayoutAfterChanges) } function encodeQueryString(q) { return Object.keys(q).map(function(k) { if (k) { var v = q[k] return encodeURIComponent(k) + (v ? '=' + encodeURIComponent(v) : '') } }).filter(function(s) { return !!s }).join('&') } function parseQueryString(qs) { var q = {} qs.replace(/^\?/,'').split('&').forEach(function(c) { var kv = c.split('=') var k = decodeURIComponent(kv[0]) if (k) { q[k] = kv[1] ? decodeURIComponent(kv[1]) : null } }) return q } var queryString = parseQueryString(location.search) var glyphNameEl = null var baseTitle = document.title var flippedLocationHash = false var singleInfo = document.querySelector('#single-info') singleInfo.parentElement.removeChild(singleInfo) singleInfo.style.display = 'block' function updateLocation() { queryString = parseQueryString(location.search) // var glyphs = document.getElementById('glyphs') var h1 = document.querySelector('h1') if (queryString.g) { if (!glyphNameEl) { glyphNameEl = document.createElement('a') glyphNameEl.href = '?g=' + encodeURIComponent(queryString.g) wrapIntLink(glyphNameEl) glyphNameEl.className = 'glyph-name' } document.title = queryString.g + ' – ' + baseTitle glyphNameEl.innerText = queryString.g h1.appendChild(glyphNameEl) document.body.classList.add('single') setScale(currentSingleScale) } else { document.title = baseTitle if (glyphNameEl) { h1.removeChild(glyphNameEl) } document.body.classList.remove('single') setScale(currentGridScale) } document.querySelector('.row.intro').style.display = ( queryString.iframe !== undefined ? 'none' : null ) if (queryString.iframe !== undefined) { document.body.classList.add('iframe') } else { document.body.classList.remove('iframe') } render() } window.onpopstate = function(ev) { updateLocation() } function navto(url) { if (location.href != url) { history.pushState({}, "Glyphs", url) updateLocation() } window.scrollTo(0,0) } function wrapIntLink(a) { a.addEventListener('click', function(ev) { if (!ev.metaKey && !ev.ctrlKey && !ev.shiftKey) { ev.preventDefault() navto(a.href) } }) } wrapIntLink(document.querySelector('h1 > a')) // keep refs to svgs so we don't have to refcount while using var svgRepository = {} ;(function(){ var svgs = document.getElementById('svgs'), svg, name for (var i = 0; i < svgs.children.length; ++i) { svg = svgs.children[i] name = svg.id.substr(4) // strip "svg-" prefix svgRepository[name] = svg } })() // Maps glyphname to glyphInfo. Only links to first found entry for a flyph. var glyphInfoMap = {} var needsUpdateGlyphInfoMap = true function render() { if (!glyphInfo) { return } var rootEl = document.getElementById('glyphs') rootEl.style.display = 'none' rootEl.innerText = '' // glyphInfo: { // "glyphs": [ // [name :string, unicode? :int|null, unicodeName? :string, color? :string|null], // ["A", 65, "LATIN CAPITAL LETTER A", "#dbeaf7"], // ... // ], // } // // Note: Glyph names might appear multiple times (always adjacent) when a glyph is // represented by multiple Unicode code points. For example: // // ["Delta", 916, "GREEK CAPITAL LETTER DELTA"], // ["Delta", 8710, "INCREMENT"], // var glyphs = glyphInfo var singleGlyph = null var lastGlyphEl = null var lastGlyphName = '' glyphInfo.forEach(function(g, i) { var name = g[0] if (needsUpdateGlyphInfoMap && !glyphInfoMap[name]) { glyphInfoMap[name] = g } if (queryString.g && name != queryString.g) { // ignore return } var glyph = renderGlyphGraphicG(g, lastGlyphName, lastGlyphEl, singleGlyph) if (glyph) { rootEl.appendChild(glyph.element) lastGlyphEl = glyph.element lastGlyphName = name if (queryString.g) { singleGlyph = glyph } } }) needsUpdateGlyphInfoMap = false if (singleGlyph) { renderSingleInfo(singleGlyph) rootEl.appendChild(singleInfo) } rootEl.style.display = null updateLayoutAfterChanges() } function renderGlyphGraphic(glyphName) { var g = glyphInfoMap[glyphName] return g ? renderGlyphGraphicG(g) : null } function renderGlyphGraphicG(g, lastGlyphName, lastGlyphEl, singleGlyph) { var name = g[0] var names, glyph var svg = svgRepository[name] if (!svg) { // ignore return null } var metrics = glyphMetrics.metrics[name] if (!metrics) { console.error('missing metrics for', name) } var info = { name: name, unicode: g[1], unicodeName: g[2], color: g[3], // These are all in 1:1 UPM (not scaled) advance: metrics.advance, left: metrics.left, right: metrics.right, width: metrics.width, element: null, } if (name == lastGlyphName) { // additional Unicode code point for same glyph glyph = lastGlyphEl names = glyph.querySelector('.names') names.innerText += ',' if (info.unicode) { var ucid = ' U+' + fmthex(info.unicode) names.innerText += ' U+' + fmthex(info.unicode) if (!queryString.g) { glyph.title += ucid } } if (info.unicodeName) { names.innerText += ' ' + info.unicodeName if (!queryString.g) { glyph.title += ' (' + info.unicodeName + ')' } } if (queryString.g) { if (singleGlyph) { if (!singleGlyph.alternates) { singleGlyph.alternates = [] } singleGlyph.alternates.push(info) } else { throw new Error('alternate glyph UC, but appears first in glyphinfo data') } } return } // console.log('svg for', name, svg.width.baseVal.value, '->', svg, '\n', info) glyph = document.createElement('a') glyph.className = 'glyph' glyph.href = '?g=' + encodeURIComponent(name) wrapIntLink(glyph) info.element = glyph if (!queryString.g) { glyph.title = name } var baseline = document.createElement('div') baseline.className = 'baseline' glyph.appendChild(baseline) names = document.createElement('div') names.className = 'names' names.innerText = name if (info.unicode) { var ucid = ' U+' + fmthex(info.unicode) names.innerText += ' U+' + fmthex(info.unicode, 4) if (!queryString.g) { glyph.title += ucid } } if (info.unicodeName) { names.innerText += ' ' + info.unicodeName if (!queryString.g) { glyph.title += ' (' + info.unicodeName + ')' } } glyph.appendChild(names) var scaledAdvance = info.advance * kSVGScale if (scaledAdvance > kGlyphSize) { glyph.style.width = scaledAdvance.toFixed(2) + 'px' } var adv = document.createElement('div') adv.className = 'advance' glyph.appendChild(adv) adv.appendChild(svg) svg.style.left = (info.left * kSVGScale).toFixed(2) + 'px' if (info.advance <= 0) { glyph.className += ' zero-width' } else { adv.style.width = scaledAdvance.toFixed(2) + 'px' } return info } function renderSingleInfo(g) { var e = singleInfo e.querySelector('.name').innerText = g.name function setv(el, name, textValue) { el.querySelector('.' + name).innerText = textValue } var unicode = e.querySelector('.unicode') function configureUnicodeView(el, g) { var a = el.querySelector('a') if (g.unicode) { a.href = "https://codepoints.net/U+" + fmthex(g.unicode, 4) } else { a.href = '' } setv(el, 'unicodeCodePoint', g.unicode ? 'U+' + fmthex(g.unicode, 4) : '–') setv(el, 'unicodeName', g.unicodeName || '') } // remove any previous alternate unicode list items var rmli = unicode while ((rmli = rmli.nextSibling)) { if (rmli.nodeType == Node.ELEMENT_NODE) { if (!rmli.parentElement || !rmli.classList.contains('unicode')) { break } rmli.parentElement.removeChild(rmli) } } configureUnicodeView(unicode, g) if (g.alternates) { var refnode = unicode.nextSibling g.alternates.forEach(function(g) { var li = unicode.cloneNode(true) configureUnicodeView(li, g) if (refnode) { unicode.parentElement.insertBefore(li, unicode.nextSibling) } else { unicode.parentElement.appendChild(li) } }) } e.querySelector('.advanceWidth').innerText = g.advance e.querySelector('.marginLeft').innerText = g.left e.querySelector('.marginRight').innerText = g.right var colorMark = e.querySelector('.colorMark') if (g.color) { colorMark.title = g.color.toUpperCase() colorMark.style.background = g.color colorMark.classList.remove('none') } else { colorMark.title = '(None)' colorMark.style.background = null colorMark.classList.add('none') } var svg = svgRepository[g.name] var svgFile = e.querySelector('.svgFile') svgFile.download = g.name + '.svg' svgFile.href = getSvgDataURI(svg) renderSingleKerning(g) } var cachedSVGDataURIs = {} function getSvgDataURI(svg, fill) { if (!fill) { fill = '' } var cached = cachedSVGDataURIs[svg.id + '-' + fill] if (!cached) { var src = svg.outerHTML.replace(/[\r\n]+/g, '') if (fill) { src = src.replace(/