inter/docs/glyphs/glyphs.js
Rasmus Andersson 2a6051f020 v3.6
2019-05-26 17:19:55 -07:00

842 lines
No EOL
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, lineRule, 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)]
lineRule = styleSheet.cssRules[styleSheet.insertRule('#glyphs .glyph .line {}', 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';
}
}
lineRule.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.json:
// { "glyphs": [
// [name :string, isEmpty: 1|0, unicode? :string|null,
// unicodeName? :string, color? :string|null],
// ["A", 0, 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", 0, "U+0916", "GREEK CAPITAL LETTER DELTA"],
// ["Delta", 0, "U+8710", "INCREMENT"],
//
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) {
// let [name, isEmpty, uc, ucName, color] = g
let name = g[0], /*isEmpty = g[1],*/ uc = g[2], ucName = g[3], color = g[4]
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: uc,
unicodeName: ucName,
color: color,
// 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+' + info.unicode
names.innerText += ' U+' + 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 line = document.createElement('div')
line.className = 'line baseline'
line.title = "Baseline"
glyph.appendChild(line)
line = document.createElement('div')
line.className = 'line x-height'
line.title = "x-height"
glyph.appendChild(line)
line = document.createElement('div')
line.className = 'line cap-height'
line.title = "Cap height"
glyph.appendChild(line)
names = document.createElement('div')
names.className = 'names'
names.innerText = name
if (info.unicode) {
var ucid = ' U+' + info.unicode
names.innerText += ' U+' + info.unicode
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+" + g.unicode
} else {
a.href = ''
}
setv(el, 'unicodeCodePoint', g.unicode ? 'U+' + g.unicode : '')
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(/<path /g, '<path fill="' + fill + '" ')
}
cached = 'data:image/svg+xml,' + src
cachedSVGDataURIs[svg.id + '-' + fill] = cached
}
return cached
}
function selectKerningPair(id, directly) {
// deselect existing
eachElement('.kernpair.selected', function(kernpair) {
eachElement(kernpair, '.g', function (glyph) {
var svgURI = getSvgDataURI(svgRepository[glyph.dataset.name])
glyph.style.backgroundImage = "url('" + svgURI + "')"
})
kernpair.classList.remove('selected')
})
var el = document.getElementById(id)
if (!el) {
history.replaceState({}, '', location.search)
return
}
el.classList.add('selected')
eachElement(el, '.g', function (glyph) {
var svgURI = getSvgDataURI(svgRepository[glyph.dataset.name], 'white')
glyph.style.backgroundImage = "url('" + svgURI + "')"
})
if (!directly) {
el.scrollIntoViewIfNeeded()
}
history.replaceState({}, '', location.search + '#' + id)
}
// return true if some kerning was rendered
function renderSingleKerning(g) {
var kerningList = document.getElementById('kerning-list')
kerningList.style.display = 'none'
kerningList.innerText = ''
var thisSvg = svgRepository[g.name]
var thisSvgURI = getSvgDataURI(thisSvg)
if (!thisSvg) {
kerningList.style.display = null
return false
}
var kerningAsLeft = glyphMetrics.kerningLeft[g.name] || {}
var kerningAsRight = glyphMetrics.kerningRight[g.name] || {}
var kerningAsLeftKeys = Object.keys(kerningAsLeft)
var kerningAsRightKeys = Object.keys(kerningAsRight)
if (kerningAsLeftKeys.length == 0 && kerningAsRightKeys.length == 0) {
var p = document.createElement('p')
p.className = 'empty'
p.innerText = 'No kerning'
kerningList.appendChild(p)
kerningList.style.display = null
return false
}
var lilGlyphSize = 128
var lilScale = lilGlyphSize / kGlyphSize
function mkpairGlyph(glyphName, side, kerningValue, svgURI) {
var metrics = glyphMetrics.metrics[glyphName]
var svgWidth = pxround(metrics.width * kSVGScale * lilScale)
var el = document.createElement('div')
var leftMargin = metrics.left
var rightMargin = metrics.right
if (side == 'left') {
rightMargin += kerningValue
} else {
leftMargin += kerningValue
}
leftMargin = leftMargin * kSVGScale * lilScale
rightMargin = rightMargin * kSVGScale * lilScale
el.dataset.name = glyphName
el.className = 'g ' + side
el.style.backgroundImage = "url('" + svgURI + "')"
el.style.backgroundSize = svgWidth + 'px ' + lilGlyphSize + 'px'
el.style.width = svgWidth + 'px'
el.style.height = lilGlyphSize + 'px'
el.style.marginLeft = pxround(leftMargin) + 'px'
el.style.marginRight = pxround(rightMargin) + 'px'
return el
}
function mkpairs(kerningInfo, selfName, self, side) {
var asLeftSide = side == 'left'
var idPrefix = asLeftSide ? 'kernl-' : 'kernr-'
var keys = Object.keys(kerningInfo)
var otherSide = asLeftSide ? 'right' : 'left'
keys.forEach(function(glyphName) {
var kerningValue = kerningInfo[glyphName]
var otherSvg = svgRepository[glyphName]
var pair = document.createElement('a')
pair.className = 'kernpair ' + side
pair.title = (
asLeftSide ? selfName + '/' + glyphName + ' ' + kerningValue :
glyphName + '/' + selfName + ' ' + kerningValue
)
pair.id = idPrefix + glyphName
pair.href = '#' + pair.id
pair.onclick = function(ev) {
selectKerningPair(pair.id)
ev.preventDefault()
ev.stopPropagation()
}
var otherEl = mkpairGlyph(glyphName, otherSide, kerningValue, getSvgDataURI(otherSvg))
if (asLeftSide) {
if (self.parentNode) {
pair.appendChild(self.cloneNode(true))
} else {
pair.appendChild(self)
}
pair.appendChild(otherEl)
} else {
pair.appendChild(otherEl)
if (self.parentNode) {
pair.appendChild(self.cloneNode(true))
} else {
pair.appendChild(self)
}
}
var kern = document.createElement('div')
kern.className = 'kern ' + (kerningValue < 0 ? 'neg' : 'pos')
var absKernVal = Math.abs(kerningValue)
var kernWidth = Math.max(1, pxround(absKernVal * kSVGScale * lilScale))
kern.style.width = kernWidth + 'px'
var leftMetrics = glyphMetrics.metrics[asLeftSide ? selfName : glyphName]
var leftAdvance = leftMetrics.advance
kern.style.left = pxround((leftAdvance - absKernVal) * kSVGScale * lilScale) + 'px'
pair.appendChild(kern)
var label = document.createElement('div')
label.className = 'label'
label.innerText = kerningValue
kern.appendChild(label)
if (glyphName != selfName) {
var link = document.createElement('div')
link.className = 'link'
var linkA = document.createElement('a')
linkA.href = '?g=' + encodeURIComponent(glyphName)
linkA.title = 'View ' + glyphName
linkA.innerText = '\u2197'
linkA.tabIndex = -1
wrapIntLink(linkA)
link.appendChild(linkA)
var kLinkAWidth = 16 // sync with CSS .kernpair .link a
if (asLeftSide) {
var rightMetrics = glyphMetrics.metrics[asLeftSide ? glyphName : selfName]
linkA.style.marginRight = pxround(
((rightMetrics.advance / 2) * kSVGScale * lilScale) - (kLinkAWidth / 2)
) + 'px'
} else {
linkA.style.marginLeft = pxround(
((leftMetrics.advance / 2) * kSVGScale * lilScale) - (kLinkAWidth / 2)
) + 'px'
}
pair.appendChild(link)
}
kerningList.appendChild(pair)
})
return keys.length != 0
}
var selfLeft = mkpairGlyph(g.name, 'left', 0, thisSvgURI)
var selfRight = mkpairGlyph(g.name, 'right', 0, thisSvgURI)
if (mkpairs(kerningAsLeft, g.name, selfLeft, 'left') &&
kerningAsRightKeys.length != 0)
{
var div = document.createElement('div')
div.className = 'divider'
kerningList.appendChild(div)
}
mkpairs(kerningAsRight, g.name, selfRight, 'right')
if (location.hash && location.hash.indexOf('#kern') == 0) {
var id = location.hash.substr(1)
setTimeout(function(){
selectKerningPair(id)
},1)
}
kerningList.style.display = null
return true
}
function fmthex(cp, minWidth) {
let s = cp.toString(16).toUpperCase()
while (s.length < minWidth) {
s = '0' + s
}
return s
}
document.addEventListener('keydown', function(ev) {
if (!queryString.g && (ev.metaKey || ev.ctrlKey)) {
if (ev.keyCode == 187 || ev.key == '+') {
setScale(parseFloat((currentScale + 0.1).toFixed(2)))
ev.preventDefault()
} else if (ev.keyCode == 189 || ev.key == '-') {
setScale(parseFloat((currentScale - 0.1).toFixed(2)))
ev.preventDefault()
} else if (ev.keyCode == 48 || ev.key == '0') {
setScale(queryString.g ? defaultSingleScale : defaultGridScale)
ev.preventDefault()
}
}
})
updateLocation()