1
1
mirror of https://github.com/rsms/inter.git synced 2024-12-20 22:21:38 +03:00
inter/docs/glyphs/glyphs.js

827 lines
21 KiB
JavaScript
Raw Normal View History

2017-08-28 12:36:29 +03:00
// 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
2017-08-28 21:31:42 +03:00
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)
}
2017-08-28 12:36:29 +03:00
if (!isMac) {
2017-08-28 21:31:42 +03:00
eachElement('kbd', function(e) {
2017-08-28 12:36:29 +03:00
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) {
2017-08-28 21:31:42 +03:00
glyphNameEl = document.createElement('a')
glyphNameEl.href = '?g=' + encodeURIComponent(queryString.g)
wrapIntLink(glyphNameEl)
2017-08-28 12:36:29 +03:00
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) {
2017-08-28 21:31:42 +03:00
if (location.href != url) {
history.pushState({}, "Glyphs", url)
updateLocation()
}
2017-08-28 12:36:29 +03:00
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) {
2017-08-28 21:31:42 +03:00
var a = el.querySelector('a')
a.href = "https://codepoints.net/U+" + fmthex(g.unicode, 4)
2017-08-28 12:36:29 +03:00
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 = {}
2017-08-28 21:31:42 +03:00
function getSvgDataURI(svg, fill) {
if (!fill) {
fill = ''
}
var cached = cachedSVGDataURIs[svg.id + '-' + fill]
2017-08-28 12:36:29 +03:00
if (!cached) {
2017-08-28 21:31:42 +03:00
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
2017-08-28 12:36:29 +03:00
}
return cached
}
function selectKerningPair(id, directly) {
2017-08-28 21:31:42 +03:00
// 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')
})
2017-08-28 12:36:29 +03:00
var el = document.getElementById(id)
2017-08-28 21:31:42 +03:00
if (!el) {
history.replaceState({}, '', location.search)
return
2017-08-28 12:36:29 +03:00
}
2017-08-28 21:31:42 +03:00
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()
}
2017-08-28 12:36:29 +03:00
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
2017-08-28 21:31:42 +03:00
el.dataset.name = glyphName
el.className = 'g ' + side
2017-08-28 12:36:29 +03:00
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')
2017-08-28 21:31:42 +03:00
pair.className = 'kernpair ' + side
2017-08-28 12:36:29 +03:00
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)
2017-08-28 21:31:42 +03:00
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)
}
2017-08-28 12:36:29 +03:00
kerningList.appendChild(pair)
})
return keys.length != 0
}
var selfLeft = mkpairGlyph(g.name, 'left', 0, thisSvgURI)
var selfRight = mkpairGlyph(g.name, 'right', 0, thisSvgURI)
2017-08-28 21:31:42 +03:00
if (mkpairs(kerningAsLeft, g.name, selfLeft, 'left') &&
kerningAsRightKeys.length != 0)
{
2017-08-28 12:36:29 +03:00
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()