From 08b9afdca9ce84f9fb1c86c9c793d16acee71c47 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Fri, 29 Sep 2017 11:58:16 -0700 Subject: [PATCH] Implement subpixel glyph positioning in the text demo --- demo/client/src/meshes.ts | 85 ++++++++++++++++++++++------------ demo/client/src/text-demo.ts | 89 +++++++++++++++++++++++++----------- demo/client/src/text.ts | 51 +++++++++++++-------- demo/client/src/utils.ts | 4 ++ 4 files changed, 154 insertions(+), 75 deletions(-) diff --git a/demo/client/src/meshes.ts b/demo/client/src/meshes.ts index 8366d73f..db1cfa39 100644 --- a/demo/client/src/meshes.ts +++ b/demo/client/src/meshes.ts @@ -11,7 +11,8 @@ import * as base64js from 'base64-js'; import * as _ from 'lodash'; -import { expectNotNull, panic, PathfinderError, UINT32_MAX, UINT32_SIZE } from './utils'; +import {expectNotNull, FLOAT32_SIZE, panic, PathfinderError, UINT16_SIZE} from './utils'; +import {UINT32_MAX, UINT32_SIZE} from './utils'; const BUFFER_TYPES: Meshes = { bQuads: 'ARRAY_BUFFER', @@ -172,11 +173,13 @@ export class PathfinderMeshData implements Meshes { indexIndex => indexIndex % 4 < 3); // Copy over B-quads. - let firstBQuadIndex = findFirstBQuadIndex(bQuads, pathID); + let firstBQuadIndex = findFirstBQuadIndex(bQuads, bVertexPathIDs, pathID); if (firstBQuadIndex == null) firstBQuadIndex = bQuads.length; const indexDelta = firstExpandedBVertexIndex - firstBVertexIndex; - for (let bQuadIndex = firstBQuadIndex; bQuadIndex < bQuads.length; bQuadIndex++) { + for (let bQuadIndex = firstBQuadIndex; + bQuadIndex < bQuads.length / B_QUAD_FIELD_COUNT; + bQuadIndex++) { const bQuad = bQuads[bQuadIndex]; if (bVertexPathIDs[bQuads[bQuadIndex * B_QUAD_FIELD_COUNT]] !== pathID) break; @@ -192,23 +195,48 @@ export class PathfinderMeshData implements Meshes { textGlyphIndex++; } + const expandedBQuadsBuffer = new ArrayBuffer(expandedBQuads.length * UINT32_SIZE); + const expandedBVertexLoopBlinnDataBuffer = + new ArrayBuffer(expandedBVertexLoopBlinnData.length * UINT32_SIZE); + const expandedBVertexPathIDsBuffer = + new ArrayBuffer(expandedBVertexPathIDs.length * UINT16_SIZE); + const expandedBVertexPositionsBuffer = + new ArrayBuffer(expandedBVertexPositions.length * FLOAT32_SIZE); + const expandedCoverCurveIndicesBuffer = + new ArrayBuffer(expandedCoverCurveIndices.length * UINT32_SIZE); + const expandedCoverInteriorIndicesBuffer = + new ArrayBuffer(expandedCoverInteriorIndices.length * UINT32_SIZE); + const expandedEdgeLowerCurveIndicesBuffer = + new ArrayBuffer(expandedEdgeLowerCurveIndices.length * UINT32_SIZE); + const expandedEdgeLowerLineIndicesBuffer = + new ArrayBuffer(expandedEdgeLowerLineIndices.length * UINT32_SIZE); + const expandedEdgeUpperCurveIndicesBuffer = + new ArrayBuffer(expandedEdgeUpperCurveIndices.length * UINT32_SIZE); + const expandedEdgeUpperLineIndicesBuffer = + new ArrayBuffer(expandedEdgeUpperLineIndices.length * UINT32_SIZE); + + (new Uint32Array(expandedBQuadsBuffer)).set(expandedBQuads); + (new Uint32Array(expandedBVertexLoopBlinnDataBuffer)).set(expandedBVertexLoopBlinnData); + (new Uint16Array(expandedBVertexPathIDsBuffer)).set(expandedBVertexPathIDs); + (new Float32Array(expandedBVertexPositionsBuffer)).set(expandedBVertexPositions); + (new Uint32Array(expandedCoverCurveIndicesBuffer)).set(expandedCoverCurveIndices); + (new Uint32Array(expandedCoverInteriorIndicesBuffer)).set(expandedCoverInteriorIndices); + (new Uint32Array(expandedEdgeLowerCurveIndicesBuffer)).set(expandedEdgeLowerCurveIndices); + (new Uint32Array(expandedEdgeLowerLineIndicesBuffer)).set(expandedEdgeLowerLineIndices); + (new Uint32Array(expandedEdgeUpperCurveIndicesBuffer)).set(expandedEdgeUpperCurveIndices); + (new Uint32Array(expandedEdgeUpperLineIndicesBuffer)).set(expandedEdgeUpperLineIndices); + return new PathfinderMeshData({ - bQuads: new Uint32Array(expandedBQuads).buffer as ArrayBuffer, - bVertexLoopBlinnData: new Uint32Array(expandedBVertexLoopBlinnData).buffer as - ArrayBuffer, - bVertexPathIDs: new Uint16Array(expandedBVertexPathIDs).buffer as ArrayBuffer, - bVertexPositions: new Float32Array(expandedBVertexPositions).buffer as ArrayBuffer, - coverCurveIndices: new Uint32Array(expandedCoverCurveIndices).buffer as ArrayBuffer, - coverInteriorIndices: new Uint32Array(expandedCoverInteriorIndices).buffer as - ArrayBuffer, - edgeLowerCurveIndices: new Uint32Array(expandedEdgeLowerCurveIndices).buffer as - ArrayBuffer, - edgeLowerLineIndices: new Uint32Array(expandedEdgeLowerLineIndices).buffer as - ArrayBuffer, - edgeUpperCurveIndices: new Uint32Array(expandedEdgeUpperCurveIndices).buffer as - ArrayBuffer, - edgeUpperLineIndices: new Uint32Array(expandedEdgeUpperLineIndices).buffer as - ArrayBuffer, + bQuads: expandedBQuadsBuffer, + bVertexLoopBlinnData: expandedBVertexLoopBlinnDataBuffer, + bVertexPathIDs: expandedBVertexPathIDsBuffer, + bVertexPositions: expandedBVertexPositionsBuffer, + coverCurveIndices: expandedCoverCurveIndicesBuffer, + coverInteriorIndices: expandedCoverInteriorIndicesBuffer, + edgeLowerCurveIndices: expandedEdgeLowerCurveIndicesBuffer, + edgeLowerLineIndices: expandedEdgeLowerLineIndicesBuffer, + edgeUpperCurveIndices: expandedEdgeUpperCurveIndicesBuffer, + edgeUpperLineIndices: expandedEdgeUpperLineIndicesBuffer, }); } } @@ -264,15 +292,14 @@ function copyIndices(destIndices: number[], } } -function findFirstBQuadIndex(bQuads: Uint32Array, queryPathID: number): number | null { - let low = 0, high = bQuads.length / B_QUAD_FIELD_COUNT; - while (low < high) { - const mid = low + (high - low) / 2; - const thisPathID = bQuads[mid * B_QUAD_FIELD_COUNT]; - if (queryPathID <= thisPathID) - high = mid; - else - low = mid + 1; +function findFirstBQuadIndex(bQuads: Uint32Array, + bVertexPathIDs: Uint16Array, + queryPathID: number): + number | null { + for (let bQuadIndex = 0; bQuadIndex < bQuads.length / B_QUAD_FIELD_COUNT; bQuadIndex++) { + const thisPathID = bVertexPathIDs[bQuads[bQuadIndex * B_QUAD_FIELD_COUNT]]; + if (thisPathID === queryPathID) + return bQuadIndex; } - return bQuads[low * B_QUAD_FIELD_COUNT] === queryPathID ? low : null; + return null; } diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index e19f259b..2ea219a4 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -29,7 +29,7 @@ import {calculatePixelDescent, calculatePixelRectForGlyph, PathfinderFont} from import {BUILTIN_FONT_URI, calculatePixelXMin, GlyphStore, Hint, SimpleTextLayout} from "./text"; import {assert, expectNotNull, panic, PathfinderError, scaleRect, UINT32_SIZE} from './utils'; import {unwrapNull} from './utils'; -import { MonochromePathfinderView, Timings, TIMINGS } from './view'; +import {MonochromePathfinderView, Timings, TIMINGS} from './view'; const DEFAULT_TEXT: string = `’Twas brillig, and the slithy toves @@ -206,11 +206,13 @@ class TextDemoController extends DemoAppController { const glyphStore = new GlyphStore(font, uniqueGlyphIDs); glyphStore.partition().then(result => { + const meshes = this.expandMeshes(result.meshes, uniqueGlyphIDs.length); + this.view.then(view => { this.font = font; this.layout = newLayout; this.glyphStore = glyphStore; - this.meshes = result.meshes; + this.meshes = meshes; view.attachText(); view.uploadPathColors(1); @@ -219,6 +221,15 @@ class TextDemoController extends DemoAppController { }); } + private expandMeshes(meshes: PathfinderMeshData, glyphCount: number): PathfinderMeshData { + const pathIDs = []; + for (let glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) { + for (let subpixel = 0; subpixel < SUBPIXEL_GRANULARITY; subpixel++) + pathIDs.push(glyphIndex + 1); + } + return meshes.expand(pathIDs); + } + get atlas(): Atlas { return this._atlas; } @@ -242,6 +253,10 @@ class TextDemoController extends DemoAppController { return this.hintingSelect.selectedIndex !== 0; } + get pathCount(): number { + return this.glyphStore.glyphIDs.length * SUBPIXEL_GRANULARITY; + } + protected get builtinFileURI(): string { return BUILTIN_FONT_URI; } @@ -249,7 +264,6 @@ class TextDemoController extends DemoAppController { protected get defaultFile(): string { return DEFAULT_FONT; } - } class TextDemoView extends MonochromePathfinderView { @@ -316,8 +330,7 @@ class TextDemoView extends MonochromePathfinderView { } protected pathColorsForObject(objectIndex: number): Uint8Array { - const atlasGlyphs = this.appController.atlasGlyphs; - const pathCount = atlasGlyphs.length; + const pathCount = this.appController.pathCount; const pathColors = new Uint8Array(4 * (pathCount + 1)); @@ -331,15 +344,15 @@ class TextDemoView extends MonochromePathfinderView { } protected pathTransformsForObject(objectIndex: number): Float32Array { - const glyphCount = this.appController.glyphStore.glyphIDs.length; + const pathCount = this.appController.pathCount; const atlasGlyphs = this.appController.atlasGlyphs; const pixelsPerUnit = this.appController.pixelsPerUnit; - const transforms = new Float32Array((glyphCount + 1) * 4); + const transforms = new Float32Array((pathCount + 1) * 4); for (const glyph of atlasGlyphs) { - const pathID = glyph.glyphStoreIndex + 1; - const atlasOrigin = glyph.calculatePixelOrigin(pixelsPerUnit); + const pathID = glyph.pathID; + const atlasOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit); transforms[pathID * 4 + 0] = pixelsPerUnit; transforms[pathID * 4 + 1] = pixelsPerUnit; @@ -451,7 +464,10 @@ class TextDemoView extends MonochromePathfinderView { for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++, globalGlyphIndex++) { - const rect = run.pixelRectForGlyphAt(glyphIndex, pixelsPerUnit, hint); + const rect = run.pixelRectForGlyphAt(glyphIndex, + pixelsPerUnit, + hint, + SUBPIXEL_GRANULARITY); glyphPositions.set([ rect[0], rect[3], rect[2], rect[3], @@ -494,7 +510,10 @@ class TextDemoView extends MonochromePathfinderView { let atlasGlyphs = []; for (const run of textFrame.runs) { for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) { - const pixelRect = run.pixelRectForGlyphAt(glyphIndex, pixelsPerUnit, hint); + const pixelRect = run.pixelRectForGlyphAt(glyphIndex, + pixelsPerUnit, + hint, + SUBPIXEL_GRANULARITY); if (!rectsIntersect(pixelRect, canvasRect)) continue; @@ -503,7 +522,11 @@ class TextDemoView extends MonochromePathfinderView { if (glyphStoreIndex == null) continue; - const glyphKey = new GlyphKey(glyphID); + const subpixel = run.subpixelForGlyphAt(glyphIndex, + pixelsPerUnit, + hint, + SUBPIXEL_GRANULARITY); + const glyphKey = new GlyphKey(glyphID, subpixel); atlasGlyphs.push(new AtlasGlyph(glyphStoreIndex, glyphKey)); } } @@ -519,12 +542,12 @@ class TextDemoView extends MonochromePathfinderView { this.uploadPathTransforms(1); // TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about. - const glyphCount = this.appController.glyphStore.glyphIDs.length; - const pathHints = new Float32Array((glyphCount + 1) * 4); + const pathCount = this.appController.pathCount; + const pathHints = new Float32Array((pathCount + 1) * 4); - for (let glyphID = 0; glyphID < glyphCount; glyphID++) { - pathHints[glyphID * 4 + 0] = hint.xHeight; - pathHints[glyphID * 4 + 1] = hint.hintedXHeight; + for (let pathID = 0; pathID < pathCount; pathID++) { + pathHints[pathID * 4 + 0] = hint.xHeight; + pathHints[pathID * 4 + 1] = hint.hintedXHeight; } const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints'); @@ -555,7 +578,7 @@ class TextDemoView extends MonochromePathfinderView { const hint = this.appController.createHint(); const pixelsPerUnit = this.appController.pixelsPerUnit; - const atlasGlyphIDs = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.id); + const atlasGlyphKeys = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.sortKey); const glyphTexCoords = new Float32Array(textFrame.totalGlyphCount * 8); @@ -566,7 +589,14 @@ class TextDemoView extends MonochromePathfinderView { glyphIndex++, globalGlyphIndex++) { const textGlyphID = run.glyphIDs[glyphIndex]; - const atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIDs, textGlyphID); + const subpixel = run.subpixelForGlyphAt(glyphIndex, + pixelsPerUnit, + hint, + SUBPIXEL_GRANULARITY); + + const glyphKey = new GlyphKey(textGlyphID, subpixel); + + const atlasGlyphIndex = _.sortedIndexOf(atlasGlyphKeys, glyphKey.sortKey); if (atlasGlyphIndex < 0) continue; @@ -576,7 +606,7 @@ class TextDemoView extends MonochromePathfinderView { if (atlasGlyphMetrics == null) continue; - const atlasGlyphPixelOrigin = atlasGlyph.calculatePixelOrigin(pixelsPerUnit); + const atlasGlyphPixelOrigin = atlasGlyph.calculateSubpixelOrigin(pixelsPerUnit); const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphMetrics, atlasGlyphPixelOrigin, pixelsPerUnit, @@ -674,7 +704,7 @@ class Atlas { continue; glyph.setPixelLowerLeft(nextOrigin, metrics, pixelsPerUnit); - let pixelOrigin = glyph.calculatePixelOrigin(pixelsPerUnit); + let pixelOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit); nextOrigin[0] = calculatePixelRectForGlyph(metrics, pixelOrigin, pixelsPerUnit, @@ -684,7 +714,7 @@ class Atlas { if (nextOrigin[0] > ATLAS_SIZE[0]) { nextOrigin = glmatrix.vec2.clone([1.0, shelfBottom + 1.0]); glyph.setPixelLowerLeft(nextOrigin, metrics, pixelsPerUnit); - pixelOrigin = glyph.calculatePixelOrigin(pixelsPerUnit); + pixelOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit); nextOrigin[0] = calculatePixelRectForGlyph(metrics, pixelOrigin, pixelsPerUnit, @@ -700,7 +730,7 @@ class Atlas { } // FIXME(pcwalton): Could be more precise if we don't have a full row. - this._usedSize = glmatrix.vec2.fromValues(ATLAS_SIZE[0], shelfBottom); + this._usedSize = glmatrix.vec2.clone([ATLAS_SIZE[0], shelfBottom]); } ensureTexture(gl: WebGLRenderingContext): WebGLTexture { @@ -740,10 +770,11 @@ class AtlasGlyph { this.origin = glmatrix.vec2.create(); } - calculatePixelOrigin(pixelsPerUnit: number): glmatrix.vec2 { + calculateSubpixelOrigin(pixelsPerUnit: number): glmatrix.vec2 { const pixelOrigin = glmatrix.vec2.create(); glmatrix.vec2.scale(pixelOrigin, this.origin, pixelsPerUnit); glmatrix.vec2.round(pixelOrigin, pixelOrigin); + pixelOrigin[0] += this.glyphKey.subpixel / SUBPIXEL_GRANULARITY; return pixelOrigin; } @@ -759,17 +790,23 @@ class AtlasGlyph { private setPixelOrigin(pixelOrigin: glmatrix.vec2, pixelsPerUnit: number): void { glmatrix.vec2.scale(this.origin, pixelOrigin, 1.0 / pixelsPerUnit); } + + get pathID(): number { + return this.glyphStoreIndex * SUBPIXEL_GRANULARITY + this.glyphKey.subpixel + 1; + } } class GlyphKey { readonly id: number; + readonly subpixel: number; - constructor(id: number) { + constructor(id: number, subpixel: number) { this.id = id; + this.subpixel = subpixel; } get sortKey(): number { - return this.id; + return this.id * SUBPIXEL_GRANULARITY + this.subpixel; } } diff --git a/demo/client/src/text.ts b/demo/client/src/text.ts index e631027a..72de6f5f 100644 --- a/demo/client/src/text.ts +++ b/demo/client/src/text.ts @@ -107,25 +107,36 @@ export class TextRun { return textGlyphOrigin; } - pixelRectForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint): glmatrix.vec4 { + pixelRectForGlyphAt(index: number, + pixelsPerUnit: number, + hint: Hint, + subpixelGranularity: number): + glmatrix.vec4 { const metrics = unwrapNull(this.font.metricsForGlyph(this.glyphIDs[index])); const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index, pixelsPerUnit, hint); + + textGlyphOrigin[0] *= subpixelGranularity; glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin); + textGlyphOrigin[0] /= subpixelGranularity; + return calculatePixelRectForGlyph(metrics, textGlyphOrigin, pixelsPerUnit, hint); } + subpixelForGlyphAt(index: number, + pixelsPerUnit: number, + hint: Hint, + subpixelGranularity: number): + number { + const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index, pixelsPerUnit, hint)[0]; + return Math.abs(Math.round(textGlyphOrigin * subpixelGranularity) % subpixelGranularity); + } + get measure(): number { const lastGlyphID = _.last(this.glyphIDs), lastAdvance = _.last(this.advances); if (lastGlyphID == null || lastAdvance == null) return 0.0; return lastAdvance + this.font.opentypeFont.glyphs.get(lastGlyphID).advanceWidth; } - - private pixelMetricsForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint): - PixelMetrics { - const metrics = unwrapNull(this.font.metricsForGlyph(index)); - return calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint); - } } export class TextFrame { @@ -293,25 +304,25 @@ export function calculatePixelDescent(metrics: Metrics, pixelsPerUnit: number): return Math.ceil(-metrics.yMin * pixelsPerUnit); } -function calculatePixelMetricsForGlyph(metrics: Metrics, pixelsPerUnit: number, hint: Hint): - PixelMetrics { +function calculateSubpixelMetricsForGlyph(metrics: Metrics, pixelsPerUnit: number, hint: Hint): + PixelMetrics { const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1]; return { - ascent: Math.ceil(top * pixelsPerUnit), - descent: calculatePixelDescent(metrics, pixelsPerUnit), - left: calculatePixelXMin(metrics, pixelsPerUnit), - right: Math.ceil(metrics.xMax * pixelsPerUnit), + ascent: top * pixelsPerUnit, + descent: metrics.yMin * pixelsPerUnit, + left: metrics.xMin * pixelsPerUnit, + right: metrics.xMax * pixelsPerUnit, }; } export function calculatePixelRectForGlyph(metrics: Metrics, - pixelOrigin: glmatrix.vec2, + subpixelOrigin: glmatrix.vec2, pixelsPerUnit: number, hint: Hint): glmatrix.vec4 { - const pixelMetrics = calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint); - return glmatrix.vec4.clone([pixelOrigin[0] + pixelMetrics.left, - pixelOrigin[1] - pixelMetrics.descent, - pixelOrigin[0] + pixelMetrics.right, - pixelOrigin[1] + pixelMetrics.ascent]); - } + const pixelMetrics = calculateSubpixelMetricsForGlyph(metrics, pixelsPerUnit, hint); + return glmatrix.vec4.clone([Math.floor(subpixelOrigin[0] + pixelMetrics.left), + Math.floor(subpixelOrigin[1] + pixelMetrics.descent), + Math.ceil(subpixelOrigin[0] + pixelMetrics.right), + Math.ceil(subpixelOrigin[1] + pixelMetrics.ascent)]); +} diff --git a/demo/client/src/utils.ts b/demo/client/src/utils.ts index b3e0d499..9eabeb61 100644 --- a/demo/client/src/utils.ts +++ b/demo/client/src/utils.ts @@ -10,6 +10,10 @@ import * as glmatrix from 'gl-matrix'; +export const FLOAT32_SIZE: number = 4; + +export const UINT16_SIZE: number = 2; + export const UINT32_MAX: number = 0xffffffff; export const UINT32_SIZE: number = 4;