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') // await inspector.loadFont('font-files/InterDisplay-Regular.otf')