Implement subpixel glyph positioning in the text demo

This commit is contained in:
Patrick Walton 2017-09-29 11:58:16 -07:00
parent 7e4308d52e
commit 08b9afdca9
4 changed files with 154 additions and 75 deletions

View File

@ -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<BufferType> = {
bQuads: 'ARRAY_BUFFER',
@ -172,11 +173,13 @@ export class PathfinderMeshData implements Meshes<ArrayBuffer> {
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<ArrayBuffer> {
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;
}

View File

@ -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<TextDemoView> {
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<TextDemoView> {
});
}
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<TextDemoView> {
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<TextDemoView> {
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;
}
}

View File

@ -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)]);
}

View File

@ -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;