---
layout: none
---
{% include defs.html %}{%
for file in site.static_files %}{%
assign _path = file.path | remove_first: "/inter" %}{%
if _path == "/font-files/InterVariable.ttf" %}{%
assign ttf_timestamp = file.modified_time | date: "%Y%m%d%H%M%S" %}{%
endif %}{%
endfor %}
import fontkit from "./fontkit-2.0.2.js"
const { min, max, ceil, floor } = Math
const $ = (q, el) => (el || document).querySelector(q)
const $$ = (q, el) => [].slice.call((el || document).querySelectorAll(q))
const rootElement = document.getElementById("glyphs")
const inspectorElement = rootElement.querySelector(".inspector")
const LABEL_X_OFFS = 16
const HMETRICS_LABEL_Y_OFFS = 5
// console.log("rootElement", rootElement)
// console.log("inspectorElement", inspectorElement)
// console.log("fontkit", fontkit)
// for (let el of $$(".popup-menu", rootElement)) {
// let select = $('select', el)
// let label = $('.label', el)
// label.innerText = select.selectedOptions[0].label
// select.onchange = () => label.innerText = select.selectedOptions[0].label
// }
let pixelRatio = window.devicePixelRatio || 1
function pxround(px) {
return ((px * pixelRatio) >>> 0) / pixelRatio
}
const monotime = performance.now.bind(performance)
const WGHT_MIN = 14, WGHT_MAX = 32
class GlyphInspector {
constructor() {
this.font = null
this.glyph = null
this.glyphUnicode = 0
this.defaultGlyphUnicode = 0x0041
this.selectedGlyphGridCell = null
this.defaultAxisValues = {wght: 400, opsz: WGHT_MAX}
this.axisValues = {wght: 0, opsz: 0}
this.idNameElement = $(".identification .name", rootElement)
this.idUnicodeElement = $(".identification .unicode", rootElement)
this.previewElement = $(".preview", rootElement)
this.bgcolor = getComputedStyle(rootElement).getPropertyValue('--background-color')
this.drawScheduled = false
this.fontInstanceCache = new Map()
this.draggedWghtStartTime = 0
this.hasDraggedWght = false
this.drawTime = monotime()
this.tmpcanvas = document.createElement("CANVAS")
this.tmpcanvas.width = 32
this.tmpcanvas.height = 32
this.canvas = document.createElement("CANVAS")
let canvasWrapper = $(".canvas", inspectorElement)
const onresize = (ev) => {
let w = canvasWrapper.clientWidth
let h = canvasWrapper.clientHeight
pixelRatio = window.devicePixelRatio || 1 // update global var
this.resize(w, h)
}
canvasWrapper.innerText = ''
canvasWrapper.appendChild(this.canvas)
onresize()
window.addEventListener('resize', onresize, {passive: true})
this.canvas.ondblclick = () => this.setFontInstance(this.defaultAxisValues)
this.initCursor()
const urlAnchor = document.location.hash
const urlAnchorPrefix = '#glyphs/'
let urlAnchorGlyphName = ''
if (urlAnchor.startsWith(urlAnchorPrefix) &&
urlAnchor.length > urlAnchorPrefix.length)
{
urlAnchorGlyphName = urlAnchor.substr(urlAnchorPrefix.length)
}
this.opszSlider = $('input[name="opsz"]')
this.defaultAxisValues.opsz = this.opszSlider.valueAsNumber
this.opszSlider.oninput = (ev) => {
this.setFontInstance({opsz: this.opszSlider.valueAsNumber})
}
// enable clicking on label to toggle
this.opszSlider.onclick = (ev) => ev.stopPropagation()
this.opszSlider.parentElement.onclick = (ev) => {
this.setFontInstance({opsz: this.axisValues.opsz > WGHT_MIN ? WGHT_MIN : WGHT_MAX})
}
this.opszCheckbox = $('input[name="opsz-switch"]')
this.defaultAxisValues.opsz = this.opszCheckbox.checked ? WGHT_MIN : WGHT_MAX
this.opszCheckbox.onchange = (ev) => {
this.setFontInstance({opsz: this.opszCheckbox.checked ? WGHT_MIN : WGHT_MAX})
}
let showDetailsCheckbox = $('input[name="show-details"]')
this.showDetails = showDetailsCheckbox.checked
showDetailsCheckbox.onchange = (ev) => {
this.showDetails = showDetailsCheckbox.checked
this.scheduleDraw()
if (this.showDetails)
console.log(`details of glyph "/${this.glyph.name}"`, this.glyph)
if (!this.hasDraggedWght) {
const autoHideHelpTimeout = 2000
clearTimeout(this.autoHideHelpTimer)
if (this.showDetails) {
this.draggedWghtStartTime = 0
this.autoHideHelpTimer = setTimeout(() => {
this.draggedWghtStartTime = monotime()
this.scheduleDraw()
}, autoHideHelpTimeout)
}
}
}
this.glyphGridCells = {} // uc => Element
let glyphsGrid = $(".glyph-list .content", rootElement)
let removeAnchors = document.body.clientWidth <= 500
for (let i = 0; i < glyphsGrid.children.length; i++) {
let el = glyphsGrid.children[i]
if (removeAnchors)
el.removeAttribute("name")
if (!el.dataset.cp) {
console.warn('no data-cp for glyphsGrid', el)
continue
}
let unicode = parseInt(el.dataset.cp, 16)
el.onclick = ev => {
this.setGlyphByUnicode(unicode)
history.replaceState({}, null, '#glyphs/' + el.dataset.name)
ev.stopPropagation()
ev.preventDefault()
}
this.glyphGridCells[unicode] = el
if (urlAnchorGlyphName === el.dataset.name) {
if (this.selectedGlyphGridCell)
this.selectedGlyphGridCell.classList.toggle('selected', false)
el.classList.toggle('selected', true)
this.selectedGlyphGridCell = el
this.defaultGlyphUnicode = unicode
rootElement.scrollIntoView(/*alignToTop*/true)
el.scrollIntoViewIfNeeded()
}
}
// if (removeAnchors && urlAnchorGlyphName)
// rootElement.focus()
}
loadImage(filename) { // -> Promise
let selfDirname = (new URL(import.meta.url)).pathname
selfDirname = selfDirname.substr(0, selfDirname.lastIndexOf('/'))
let img = new Image()
img.src = filename[0] == '/' ? filename : selfDirname + '/' + filename
return new Promise((res, rej) => {
img.onload = () => { res(img) }
img.onerror = err => { rej(err) }
})
}
initCursor() {
this.cursor = {
x: 0,
y: 0,
dragOriginAxisValues: {...this.axisValues},
dragOriginX: 0,
dragOriginY: 0,
dtime: 0,
active: false,
dragging: false,
}
// this.cursorImage = null
// this.loadImage('cursor-glyph-inspector.svg').then(im => {
// this.cursorImage = im
// this.scheduleDraw()
// })
this.canvas.addEventListener('pointerover', ev => {
// Fired when a pointer is moved into an element's hit test boundaries
//console.log(ev.type, ev.pointerType, ev)
this.cursorActivate(ev)
})
// this.canvas.addEventListener('pointerenter', ev => {
// // Fired when a pointer is moved into the hit test boundaries of an element
// // or one of its descendants, including as a result of a pointerdown event
// // from a device that does not support hover
// console.log(ev.type, ev.pointerType, ev)
// })
this.canvas.addEventListener('pointerdown', ev => {
// Fired when a pointer becomes "active buttons state"
//console.log(ev.type, ev.pointerType, ev)
this.cursorDragBegin(ev)
})
// this.canvas.addEventListener('gotpointercapture', ev => {
// console.log(ev.type, ev.pointerType, ev)
// })
this.canvas.addEventListener('pointermove', ev => {
// Fired when a pointer changes coordinates.
// This event is also used if the change in pointer state cannot be reported
// by other events
//console.log(ev.type, ev.pointerType, ev)
this.cursorMoved(ev)
})
this.canvas.addEventListener('pointerup', ev => {
// Fired when a pointer is no longer "active buttons state"
//console.log(ev.type, ev.pointerType, ev)
this.cursorDragEnd(ev)
})
this.canvas.addEventListener('pointercancel', ev => {
// A browser fires this event if it concludes the pointer will no longer be
// able to generate events (for example the related device is deactivated)
//console.log(ev.type, ev.pointerType, ev)
if (this.cursor.dragging)
this.cursorDragEnd(ev)
if (this.cursor.active)
this.cursorDeactivate(ev)
})
this.canvas.addEventListener('pointerout', ev => {
// Fired for several reasons, including:
// - pointer is moved out of the hit test boundaries of an element
// - firing the pointerup event for a device that does not support hover
// - after firing the pointercancel event
// - when a pen stylus leaves the hover range detectable by the digitizer
//console.log(ev.type, ev.pointerType, ev)
if (this.cursor.active)
this.cursorDeactivate(ev)
})
// this.canvas.addEventListener('pointerleave', ev => {
// // Fired when a pointer is moved out of the hit test boundaries of an element.
// // For pen devices, this event is fired when the stylus leaves the hover range
// // detectable by the digitizer
// console.log(ev.type, ev.pointerType, ev)
// })
}
cursorActivate(ev) {
this.cursor.active = true
this.scheduleDraw()
}
cursorDeactivate(ev) {
this.cursor.active = false
this.scheduleDraw()
}
cursorMoved(ev) {
this.cursor.x = ev.offsetX
this.cursor.y = ev.offsetY
// // if we draw our own cursor:
// if (!this.cursor.active)
// return
// this.scheduleDraw()
if (!this.cursor.dragging)
return
let w = this.canvas.width / pixelRatio
//let h = this.canvas.height / pixelRatio
let dx_dp = this.cursor.x - this.cursor.dragOriginX
//let dy_dp = this.cursor.y - this.cursor.dragOriginY
// ratio of half canvas
// movement from center to edge = 1.0
// movement from edge to edge = 2.0
let dx = dx_dp / (w/2)
//let dy = dy_dp / (h/2)
//let d = Math.sqrt(dx*dx + dy*dy)
//let d = (dx + dy) / 2
let d = dx
let {wght, opsz} = this.cursor.dragOriginAxisValues
wght = wght + d*800
if (ev.shiftKey)
wght = Math.round(wght / 100) * 100
wght = max(100, min(900, wght))
// opsz = max(WGHT_MIN, min(WGHT_MAX, opsz))
this.hasDraggedWght = true
if (this.draggedWghtStartTime == 0)
this.draggedWghtStartTime = monotime()
clearTimeout(this.autoHideHelpTimer)
this.setFontInstance({wght, opsz})
}
cursorDragBegin(ev) {
if (!this.cursor.active)
console.warn("pointerdown without a prior pointerover")
this.canvas.setPointerCapture(ev.pointerId)
this.cursor.dragOriginX = ev.offsetX
this.cursor.dragOriginY = ev.offsetY
this.cursor.dragOriginAxisValues = {...this.axisValues}
this.cursor.dragging = true
this.scheduleDraw()
this.cancelEvent = ev => {
ev.preventDefault()
ev.stopPropagation()
return false
}
//document.addEventListener('touchstart', this.cancelEvent, {passive:false,capture:true})
//document.addEventListener('touchbegin', this.cancelEvent, {passive:false,capture:true})
//document.addEventListener('scroll', this.cancelEvent, {passive:false,capture:true})
//document.style.overflow = 'hidden'
}
cursorDragEnd(ev) {
//document.removeEventListener('touchstart', this.cancelEvent, {passive:false,capture:true})
//document.removeEventListener('touchbegin', this.cancelEvent, {passive:false,capture:true})
//document.removeEventListener('scroll', this.cancelEvent, {passive:false,capture:true})
//document.style.overflow = null
this.cursor.dragging = false
this.scheduleDraw()
}
drawCursor(g, w, h) {
if (!this.cursor.active)
return
let {x, y} = this.cursor
// if (this.cursorImage) {
// let im = this.cursorImage
// g.drawImage(im, x - im.width/2, y - im.width/2)
// }
// g.beginPath()
// g.moveTo(x, y-8)
// g.lineTo(x, y+8)
// g.moveTo(x-8, y)
// g.lineTo(x+8, y)
// g.strokeStyle = 'red'
// g.lineWidth = 1.0
// g.stroke()
if (this.cursor.dragging) {
g.fillStyle = 'white'
g.strokeStyle = 'rgba(0,0,0,0.4)'
g.lineWidth = 1.5
// let label = `${this.axisValues.opsz}`
// g.textAlign = 'left'
// g.strokeText(label, x+10, y+4)
// g.fillText(label, x+10, y+4)
let label = `${this.axisValues.wght.toFixed(1)}`
g.textAlign = 'center'
g.strokeText(label, x, y+22)
g.lineWidth = 2.0
g.strokeStyle = 'rgba(0,0,0,0.1)'
g.strokeText(label, x, y+23)
g.fillText(label, x, y+22)
}
}
snapToGrid(value) {
const gridSize = 16
return value - (value % gridSize)
}
drawHMetricLine(g, w, h, y, label) {
g.beginPath()
g.moveTo(0, y)
g.lineTo(w, y)
g.strokeStyle = 'white'
g.lineWidth = 1.0
g.stroke()
g.fillStyle = 'white'
g.strokeStyle = this.bgcolor
g.lineWidth = 3.0
g.textAlign = 'left'
if (this.showDetails)
g.strokeText(label, LABEL_X_OFFS, y - HMETRICS_LABEL_Y_OFFS)
g.fillText(label, LABEL_X_OFFS, y - HMETRICS_LABEL_Y_OFFS)
}
drawPathDetails(glyph, g, w, h, scale) {
let anchors = []
let handles = []
let x1, y1, startX, startY
let commands = glyph.path.commands
let cmd2
// g.fillStyle = 'blue'
// TODO: consider converting quadratic to cubic bezier paths to display
// actual design-time paths
g.save()
g.beginPath()
for (let i = 0; i < commands.length; i++) {
let { command, args } = commands[i]
//console.log(command, ...args)
//g.fillText(`${i}`, args[0] * scale, -args[1] * scale)
switch (command) {
case "closePath":
if (anchors.length > 0)
anchors[anchors.length-1].push(/*isStartingPoint*/true)
//g.closePath()
break
case "moveTo":
x1 = args[0] * scale
y1 = args[1] * -scale
anchors.push([x1, y1])
startX = x1
startY = y1
g.moveTo(x1, y1)
break
case "lineTo":
x1 = args[0] * scale
y1 = args[1] * -scale
anchors.push([x1, y1])
g.moveTo(x1, y1)
break
case "quadraticCurveTo":
x1 = args[2] * scale
y1 = args[3] * -scale
anchors.push([x1, y1])
handles.push([args[0] * scale, args[1] * -scale])
g.lineTo(args[0] * scale, args[1] * -scale)
g.lineTo(x1, y1)
break
case "bezierCurveTo":
x1 = args[4] * scale
y1 = -args[5] * scale
anchors.push([x1, y1])
handles.push([args[0] * scale, -args[1] * scale])
handles.push([args[2] * scale, -args[3] * scale])
break
default:
console.warning("unhandled draw command:", command)
}
}
g.lineWidth = 1
g.strokeStyle = 'rgba(0,0,0,0.3)'
//g.strokeStyle = 'red'
g.stroke()
let radius = 3
g.strokeStyle = 'black'
g.fillStyle = 'white'
for (let [x, y, isStartingPoint] of anchors) {
g.beginPath()
g.ellipse(x, y, radius, radius, 0, 0, 360)
if (isStartingPoint) {
g.fillStyle = 'black'
g.fill()
g.fillStyle = 'white'
} else {
g.fill()
g.stroke()
}
}
g.strokeStyle = 'black'
g.fillStyle = 'black'
for (let [x, y] of handles) {
g.beginPath()
g.ellipse(x, y, 2, 2, 0, 0, 360)
g.fill()
}
g.restore()
}
makePixelDrawing(w, h, f) { // -> Promise
let canvas = this.tmpcanvas
canvas.width = w
canvas.height = h
let g = canvas.getContext('2d')
g.clearRect(0,0,w,h)
let imageData = g.getImageData(0, 0, w, h)
f(g, w, h, imageData)
g.putImageData(imageData, 0, 0)
if (navigator.userAgent.indexOf('Safari') != -1) {
// TODO FIXME: g.createPattern errors in Safari when we pass ImageBitmap
return Promise.resolve(this.tmpcanvas)
}
return createImageBitmap(this.tmpcanvas, 0, 0, w, h)
}
getPattern() { // -> ImageBitmap|null
if (this.pattern1)
return this.pattern1
if (this.pattern1Promise)
return null
this.pattern1Promise = this.makePixelDrawing(8, 8, (g, w, h, imageData) => {
const setpx = (x,y) => {
let n = (y*w + x) * 4
imageData.data[n] = 255 // r
imageData.data[n+1] = 255 // g
imageData.data[n+2] = 255 // b
imageData.data[n+3] = 160 // a
}
setpx(0, 0) // patch line intersecting at corner (when 2px wide, only)
for (let x = w-1, y = 0; y < h; x--, y++) {
setpx(x, y)
setpx(x, y+1) // 2px wide
}
}).then(image => {
this.pattern1 = image
this.scheduleDraw()
})
return null
}
drawGlyphBounds(glyph, g, w, h, x, xmax, ascender, descender, scale) {
// g.beginPath()
// g.moveTo(x, ascender)
// g.lineTo(x, descender)
// g.moveTo(xmax, ascender)
// g.lineTo(xmax, descender)
// g.strokeStyle = 'white'
// g.lineWidth = 1
// g.stroke()
//let pattern = g.createPattern(image, "repeat")
let patternImage = this.getPattern()
if (patternImage) {
const px = pixelRatio
g.save()
let pattern = g.createPattern(patternImage, "repeat")
g.scale(1/px, 1/px)
g.fillStyle = pattern
g.fillRect(0, ascender*px, x*px, (descender - ascender)*px)
g.fillRect(xmax*px, ascender*px, (w - xmax)*px, (descender - ascender)*px)
g.restore()
}
let { maxX, minX } = glyph.bbox
maxX = maxX >> 0 // should always be integer, but floor just in case
minX = minX >> 0 // should always be integer, but floor just in case
let advanceWidth = glyph.advanceWidth >>> 0
let lsb = minX
let rsb = advanceWidth - (maxX - minX) - minX
let y = descender + 4
g.fillStyle = 'black'
g.strokeStyle = 'black'
g.lineWidth = 1
// advance width
g.textAlign = 'center'
g.fillText(`${advanceWidth}`, pxround(w/2), y + 24)
if (advanceWidth == 0) {
x = w/2
g.beginPath()
g.moveTo(x, y)
g.lineTo(x, y+8)
g.stroke()
} else {
// LSB
let x2 = lsb * scale
g.beginPath()
g.moveTo(x, y)
g.lineTo(x, y+8)
g.moveTo(pxround(x + x2), y)
g.lineTo(pxround(x + x2), y+8)
g.stroke()
g.textAlign = 'center'
g.fillText(`${lsb}`, pxround(x + x2/2), y + 24)
// RSB
x2 = rsb * scale
g.beginPath()
g.moveTo(xmax, y)
g.lineTo(xmax, y+8)
g.moveTo(pxround(xmax - x2), y)
g.lineTo(pxround(xmax - x2), y+8)
g.stroke()
g.textAlign = 'center'
g.fillText(`${rsb}`, pxround(xmax - x2/2), y + 24)
}
}
drawAxisValues(g, w, h, ascender) {
let {wght, opsz} = this.axisValues
g.save()
let x = w - LABEL_X_OFFS
let y = ascender/2 + 4
g.font = '400 14px InterVariable, sans-serif'
g.fillStyle = 'black'
g.textAlign = 'right'
g.fillText(`wght ${wght.toFixed(1)}`, x, y)
x -= w/4
g.fillText(`opsz ${opsz.toFixed(1)}`, x, y)
let helpOpacity = 1
if (this.draggedWghtStartTime > 0) {
let age = this.drawTime - this.draggedWghtStartTime
helpOpacity = 1.0 - age/200
if (helpOpacity > 0)
this.scheduleDraw()
}
if (helpOpacity > 0) {
let label = `⟷ drag to adjust weight`
g.font = '500 18px InterVariable, sans-serif'
g.textAlign = 'center'
let textMetrics = g.measureText(label)
g.fillStyle = `rgba(255,255,255,${helpOpacity})`
const bgpadding_x = 8, bgpadding_y = 6
const cornerRadius = 4
g.beginPath()
g.roundRect(
w/2 - textMetrics.actualBoundingBoxLeft - bgpadding_x,
h/2 - textMetrics.actualBoundingBoxAscent - bgpadding_y - 1,
textMetrics.width + bgpadding_x*2,
textMetrics.actualBoundingBoxAscent
+ textMetrics.actualBoundingBoxDescent + bgpadding_y*2,
cornerRadius)
g.fill()
g.fillStyle = `rgba(0,0,0,${helpOpacity})`
g.fillText(label, w/2, h/2)
}
g.restore()
}
drawDebugXLine(g, w, h, x, name) {
g.save()
g.beginPath()
g.moveTo(x, 0)
g.lineTo(x, h)
g.strokeStyle = 'red'
g.lineWidth = 1
g.stroke()
g.textAlign = 'center'
g.fillStyle = 'red'
g.strokeStyle = this.bgcolor
g.lineWidth = 3.0
g.strokeText(`${name}=${x}`, x, h/2)
g.fillText(`${name}=${x}`, x, h/2)
g.restore()
}
drawDebugYLine(g, w, h, y, name) {
g.save()
g.beginPath()
g.moveTo(0, y)
g.lineTo(w, y)
g.strokeStyle = 'red'
g.lineWidth = 1
g.stroke()
g.textAlign = 'center'
g.fillStyle = 'red'
g.strokeStyle = this.bgcolor
g.lineWidth = 3.0
g.strokeText(`${name}=${y}`, w/2, y+4)
g.fillText(`${name}=${y}`, w/2, y+4)
g.restore()
}
drawGlyph(glyph, g, w, h) {
const margin = 16
// for debugging margin:
//const margin = performance.now()/30 % 32; requestAnimationFrame(() => this.draw())
// for debugging scalable layout:
//h -= performance.now()/30 % 100; requestAnimationFrame(() => this.draw())
const fontInstance = glyph._font
const upm = fontInstance.unitsPerEm
const { maxX, maxY, minX, minY } = fontInstance.bbox
let maxGlyphHeight = maxY - minY
let maxGlyphWidth = max(upm, glyph.advanceWidth * 1.1) // maxX-minX is very large
let boundsW = w - margin*2
let boundsH = h - margin*2
let scale = min(boundsW/maxGlyphWidth, boundsH/maxGlyphHeight)
let glyphWidth = glyph.advanceWidth * scale
let x = pxround((boundsW - glyphWidth) / 2 + margin)
let xmax = pxround((boundsW + glyphWidth) / 2 + margin)
let baseline = pxround(upm * scale)
let capHeight = pxround((upm - fontInstance.capHeight) * scale)
let xHeight = pxround((upm - fontInstance.xHeight) * scale)
let ascender = pxround((upm - fontInstance.ascent) * scale)
let descender = pxround((upm - fontInstance.descent) * scale)
g.save()
let verticalOffset = pxround( (h - margin)*maxY/maxGlyphHeight - baseline - margin)
g.translate(0, verticalOffset)
// this.drawDebugXLine(g, w, h, x, 'x')
// this.drawDebugXLine(g, w, h, xmax, 'xmax')
// draw bounds (side bearings)
if (this.showDetails) {
this.drawGlyphBounds(
glyph, g, w, h, x, xmax, ascender, descender, scale)
}
// draw horizontal metrics
if (this.showDetails) {
this.drawHMetricLine(g, w, h, baseline, "Baseline")
this.drawHMetricLine(g, w, h, capHeight, "Cap height")
this.drawHMetricLine(g, w, h, xHeight, "x-height")
this.drawHMetricLine(g, w, h, ascender, "Ascender")
this.drawHMetricLine(g, w, h, descender, "Descender")
} else {
const yoffs = 1.0 - (1.0 / pixelRatio)
this.drawHMetricLine(g, w, h, baseline - yoffs, "Baseline")
this.drawHMetricLine(g, w, h, capHeight + yoffs, "Cap height")
this.drawHMetricLine(g, w, h, xHeight + yoffs, "x-height")
}
// draw glyph
g.translate(x, baseline)
if (glyph.advanceWidth >>> 0 == 0) {
// center zero-width glyphs, regardless of LSB
let maxX = glyph.bbox.maxX >> 0
let minX = glyph.bbox.minX >> 0
g.translate((-minX - (maxX-minX)/2) * scale, 0)
}
g.beginPath()
for (let i = 0, len = glyph.path.commands.length; i < len; i++) {
let cmd = glyph.path.commands[i]
let x1 = cmd.args[0] * scale
let y1 = -cmd.args[1] * scale
let x2, y2, x3, y3
if (cmd.args.length > 2) {
x2 = cmd.args[2] * scale
y2 = -cmd.args[3] * scale
if (cmd.args.length > 4) {
x3 = cmd.args[4] * scale
y3 = -cmd.args[5] * scale
}
}
g[cmd.command](x1, y1, x2, y2, x3, y3)
}
if (this.showDetails) {
g.fillStyle = 'rgba(0,0,0,0.1)'
g.fill()
g.strokeStyle = 'black'
g.lineWidth = 1
g.stroke()
} else {
g.fillStyle = 'black'
g.fill()
}
if (this.showDetails)
this.drawPathDetails(glyph, g, w, h, scale)
g.restore()
if (this.showDetails)
this.drawAxisValues(g, w, h, ascender + verticalOffset)
}
drawGrid(g, w, h, size) {
const upm = this.fontInstance.unitsPerEm
let rows = ceil(h / size)
let cols = ceil(w / size)
g.beginPath()
for (let row = 0; row < rows; row++) {
g.moveTo(0, row*size)
g.lineTo(w, row*size)
}
for (let col = 0; col < cols; col++) {
g.moveTo(col*size, 0)
g.lineTo(col*size, h)
}
g.strokeStyle = 'rgba(0,0,0,0.3)'
g.lineWidth = 1.0
g.stroke()
}
draw(time) {
const g = this.canvas.getContext('2d')
const w = this.canvas.width / pixelRatio
const h = this.canvas.height / pixelRatio
this.drawTime = monotime() // in case monotime() != time
this.drawScheduled = false
g.resetTransform()
g.font = '500 12px InterVariable'
g.textRendering = "geometricPrecision"
g.scale(pixelRatio, pixelRatio)
g.clearRect(0, 0, w, h)
if (!this.glyph)
return
// g.fillStyle = '#ccc'; g.fillRect(0, 0, w, h) // debug
// this.drawGrid(g, w, h, 8)
this.drawGlyph(this.glyph, g, w, h)
// this.drawCursor(g, w, h)
}
scheduleDraw() {
if (this.drawScheduled)
return
this.drawScheduled = true
requestAnimationFrame(time => this.draw(time))
}
resize(w, h) {
this.canvas.width = w * pixelRatio
this.canvas.height = h * pixelRatio
this.canvas.style.width = `${w}px`
this.canvas.style.height = `${h}px`
// w = this.tmpcanvas.width
// h = this.tmpcanvas.height
// this.tmpcanvas.width = w * pixelRatio
// this.tmpcanvas.height = h * pixelRatio
// this.tmpcanvas.style.width = `${w}px`
// this.tmpcanvas.style.height = `${h}px`
// this.tmpcanvas.getContext('2d').scale(1/pixelRatio, 1/pixelRatio)
this.scheduleDraw()
}
glyphByUnicode(unicode) {
return this.fontInstance.glyphForCodePoint(unicode)
}
setGlyphByUnicode(unicode) {
this.glyph = this.glyphByUnicode(unicode)
this.scheduleDraw()
if (this.glyphUnicode == unicode) {
// same logical glyph, just for a different instance
return
}
this.glyphUnicode = unicode
//console.log("this.glyph", this.glyph)
if (this.selectedGlyphGridCell)
this.selectedGlyphGridCell.classList.toggle('selected', false)
this.selectedGlyphGridCell = this.glyphGridCells[unicode]
if (this.selectedGlyphGridCell)
this.selectedGlyphGridCell.classList.toggle('selected', true)
// update info
this.idNameElement.innerHTML = (
this.selectedGlyphGridCell ? this.selectedGlyphGridCell.dataset.name :
this.glyph.name
) //+ ' ' + String.fromCodePoint(unicode)
this.idUnicodeElement.innerText = 'U+' + '0'.repeat(
unicode < 0x10 ? 3 :
unicode < 0x100 ? 2 :
unicode < 0x1000 ? 1 :
0
) + unicode.toString(16).toUpperCase()
this.previewElement.innerText = String.fromCodePoint(unicode)
}
updateIdentificationInfo() {
let wght = this.axisValues.wght >>> 0
let opsz = this.axisValues.opsz >>> 0
if (!this.previewAxisValues ||
this.previewAxisValues.wght != wght ||
this.previewAxisValues.opsz != opsz)
{
this.previewAxisValues = {wght, opsz}
clearTimeout(this.previewUpdateTimer)
this.previewUpdateTimer = setTimeout(() => {
rootElement.style.setProperty('--inspector-wght', wght)
rootElement.style.setProperty('--inspector-opsz', opsz)
},10)
}
}
getFontInstance(axisValues) {
// note: there's no perf/memory benefit to caching instances here
try {
let fontInstance = this.font.getVariation(axisValues)
// [BUG] workaround for bug in fontkit 2.0.2 where xHeight is
// not correctly loaded for the instance
const xHeightOpszMax = 1056 // "display"
const xHeightOpszMin = 1118 // "text"
// get opsz as range [0-1]
const opszMin = this.font.variationAxes.opsz.min
const opszMax = this.font.variationAxes.opsz.max
let opsz = max(opszMin, min(opszMax, axisValues.opsz))
opsz = (opsz - opszMin) / (opszMax - opszMin) // 0.0=min, 1.0=max
// set correct xHeight
Object.defineProperty(fontInstance, 'xHeight', {
value: xHeightOpszMin + opsz*(xHeightOpszMax - xHeightOpszMin),
})
//console.log("fontInstance:", fontInstance)
return fontInstance
} catch (err) {
console.warn('font.getVariation failed:', err)
return this.font
}
}
setFontInstance(axisValues) {
axisValues = {...this.axisValues, ...axisValues}
if (this.axisValues.wght == axisValues.wght &&
this.axisValues.opsz == axisValues.opsz)
{
//console.debug("this.axisValues unchanged", axisValues, this.axisValues)
return
}
this.axisValues = axisValues
this.fontInstance = this.getFontInstance(this.axisValues)
this.setGlyphByUnicode(this.glyphUnicode ? this.glyphUnicode : this.defaultGlyphUnicode)
const opszMin = this.font.variationAxes.opsz.min
const opszMax = this.font.variationAxes.opsz.max
this.opszCheckbox.checked = this.axisValues.opsz < opszMin+(opszMax-opszMin)/2
this.opszSlider.valueAsNumber = this.axisValues.opsz
this.updateIdentificationInfo()
}
setFont(font) {
this.font = font
this.setFontInstance(this.defaultAxisValues)
}
async loadFont(url) {
let data = await fetch(url).then(r => r.arrayBuffer())
let font = fontkit.create(new Uint8Array(data))
//console.log(`loadFont(${url}) =>`, font)
this.setFont(font)
// let wght = 100
// let inc = true
// setInterval(x => {
// this.setFontInstance({wght, opsz: WGHT_MAX})
// if (inc) {
// wght += 10
// if (wght > 900) {
// wght = 900
// inc = false
// }
// } else {
// wght -= 10
// if (wght < 100) {
// wght = 100
// inc = true
// }
// }
// }, 20)
}
}
let inspector = new GlyphInspector()
await inspector.loadFont('font-files/InterVariable.ttf?v={{ttf_timestamp}}')
// await inspector.loadFont('font-files/InterDisplay-Regular.otf')