From 8528aeef1727071d342bc0640147ec31a1a3db4a Mon Sep 17 00:00:00 2001 From: rod-lin Date: Wed, 27 Nov 2019 22:37:49 -0600 Subject: [PATCH] improve text metrics measurement and compatibility with other browsers --- package.json | 3 +- src/yterm/canvas.ts | 23 ++++++----- src/yterm/metrics.ts | 95 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 src/yterm/metrics.ts diff --git a/package.json b/package.json index d025d16..6a79b03 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.0.1", "description": "", "scripts": { - "build": "npx webpack", + "build": "npx webpack --mode production", + "build-dev": "npx webpack --mode development", "test": "npx tsc --noEmit && npx mocha -r ts-node/register tests/**/*.test.ts", "coverage": "npx nyc -r text -r lcov -n src -e .ts npm run test", "demo": "npx ts-node demo/server.ts" diff --git a/src/yterm/canvas.ts b/src/yterm/canvas.ts index e27c381..84ef063 100644 --- a/src/yterm/canvas.ts +++ b/src/yterm/canvas.ts @@ -1,6 +1,7 @@ import { assert } from "./utils"; import { Renderer, Block, Intensity, TextStyle } from "./renderer"; import { ColorScheme, TangoColorScheme } from "./schemes"; +import { TextMetrics } from "./metrics"; /** * Abstraction for font @@ -25,6 +26,10 @@ export class Font { getContextFont (style = "normal", weight = "normal"): string { return `${style} ${weight} ${this.getSize()}px ${this.getFamily()}`; } + + measure (): TextMetrics { + return new TextMetrics(this.family, this.size); + } } /** @@ -306,7 +311,7 @@ class TextSelector { const { fontWidth, fontHeight } = this.canvasRenderer.getFontDimensioin(); const { columns } = this.canvasRenderer.getGridSize(); - assert(startRow <= endRow, "startRow > endRow"); + assert(startRow <= endRow, `invalid start and end row ${startRow}, ${endRow}`); this.selectionLayerContext.save(); @@ -686,17 +691,17 @@ export class CanvasRenderer extends Renderer { /** * Set font and measure the dimension of the current font + * TODO: invetigate compatibility */ private setFont (font: Font) { - this.textLayerContext.save(); - this.textLayerContext.font = font.getContextFont(); - const metrics = this.textLayerContext.measureText("█"); - this.textLayerContext.restore(); - this.font = font; - this.fontWidth = metrics.width; - this.fontHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; - this.fontDescent = metrics.actualBoundingBoxDescent; + const metrics = this.font.measure(); + + this.fontDescent = metrics.measureMaxDescent(); + this.fontWidth = metrics.measureMaxWidth() + 1; + this.fontHeight = metrics.measureMaxHeight() + this.fontDescent; + + console.log(this.fontWidth, this.fontHeight, this.fontDescent); } /** diff --git a/src/yterm/metrics.ts b/src/yterm/metrics.ts new file mode 100644 index 0000000..aa6b676 --- /dev/null +++ b/src/yterm/metrics.ts @@ -0,0 +1,95 @@ +/** + * Utility class for measruing text metrics + * measuring tricks from https://github.com/soulwire/FontMetrics + */ +export class TextMetrics { + private fontFamily: string; + private fontSize: number; + + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + + constructor (fontFamily: string, fontSize: number) { + this.fontFamily = fontFamily; + this.fontSize = fontSize; + + this.canvas = document.createElement("canvas"); + + // setting the dimension to 2 * fontSize in case of any oferflow + this.canvas.width = fontSize * 2; + this.canvas.height = fontSize * 2; + + this.ctx = this.canvas.getContext("2d")!; + } + + /** + * max distance from any ascender to any descender + */ + measureMaxHeight (): number { + return this.getDimension("h").height - + this.getDimension("x").height + + this.getDimension("g").height; + } + + measureMaxWidth (): number { + return this.getDimension("W").width; + } + + measureMaxDescent (): number { + return this.getDimension("g").height - + this.getDimension("x").height; + } + + private getCanvasFont (): string { + return `${this.fontSize}px ${this.fontFamily}`; + } + + private clearCanvas () { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + private pixelIndexToPosition (index: number): {x: number, y: number} { + index = Math.floor(index / 4); + return { + x: index % this.canvas.width, + y: Math.floor(index / this.canvas.width) + }; + } + + private getDimension (text: string): { width: number, height: number } { + this.clearCanvas(); + + this.ctx.font = this.getCanvasFont(); + this.ctx.fillStyle = "rgba(255, 255, 255, 255)"; + this.ctx.fillText(text, this.fontSize, this.fontSize); + + const pixels = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data; + + console.log(pixels); + + let minX = this.canvas.width, maxX = 0; + let minY = this.canvas.height, maxY = 0; + + for (let i = 0; i < pixels.length; i++) { + if (pixels[i] != 0) { + const {x, y} = this.pixelIndexToPosition(i); + + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + + console.log({ + text: text, + width: maxX - minX, + height: maxY - minY + }); + + return { + width: maxX - minX, + height: maxY - minY + }; + } +};