From 60ff71be8472bb67cf89b7db0abd7faccd7be1b4 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Fri, 29 Sep 2017 22:12:09 -0700 Subject: [PATCH] Add an experimental implementation of macOS-like font dilation Following Apple's earlier terminology, this is exposed as "strong" subpixel AA. --- demo/client/html/text-demo.html.hbs | 14 ++--- demo/client/src/3d-demo.ts | 3 +- demo/client/src/aa-strategy.ts | 4 +- demo/client/src/app-controller.ts | 18 ++++--- demo/client/src/benchmark.ts | 3 +- demo/client/src/ecaa-strategy.ts | 10 ++-- demo/client/src/ssaa-strategy.ts | 10 ++-- demo/client/src/svg-demo.ts | 4 +- demo/client/src/text-demo.ts | 81 +++++++++++++++++++++-------- demo/client/src/text.ts | 15 ++++-- demo/client/src/view.ts | 8 +-- 11 files changed, 112 insertions(+), 58 deletions(-) diff --git a/demo/client/html/text-demo.html.hbs b/demo/client/html/text-demo.html.hbs index ea9b5539..39d17ce1 100644 --- a/demo/client/html/text-demo.html.hbs +++ b/demo/client/html/text-demo.html.hbs @@ -54,12 +54,14 @@ -
- +
+ +
diff --git a/demo/client/src/3d-demo.ts b/demo/client/src/3d-demo.ts index 07b0366b..13fb822d 100644 --- a/demo/client/src/3d-demo.ts +++ b/demo/client/src/3d-demo.ts @@ -14,6 +14,7 @@ import * as opentype from "opentype.js"; import {mat4, vec2} from "gl-matrix"; import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; +import {SubpixelAAType} from "./aa-strategy"; import {DemoAppController} from "./app-controller"; import PathfinderBufferTexture from "./buffer-texture"; import {PerspectiveCamera} from "./camera"; @@ -285,7 +286,7 @@ class ThreeDView extends PathfinderDemoView { protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean): + subpixelAA: SubpixelAAType): AntialiasingStrategy { if (aaType !== 'ecaa') return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); diff --git a/demo/client/src/aa-strategy.ts b/demo/client/src/aa-strategy.ts index bc2e9667..64b2c66c 100644 --- a/demo/client/src/aa-strategy.ts +++ b/demo/client/src/aa-strategy.ts @@ -14,6 +14,8 @@ import {PathfinderDemoView} from './view'; export type AntialiasingStrategyName = 'none' | 'ssaa' | 'ecaa'; +export type SubpixelAAType = 'none' | 'medium' | 'strong'; + export abstract class AntialiasingStrategy { // True if direct rendering should occur. shouldRenderDirect: boolean; @@ -51,7 +53,7 @@ export abstract class AntialiasingStrategy { export class NoAAStrategy extends AntialiasingStrategy { framebufferSize: glmatrix.vec2; - constructor(level: number, subpixelAA: boolean) { + constructor(level: number, subpixelAA: SubpixelAAType) { super(); this.framebufferSize = glmatrix.vec2.create(); } diff --git a/demo/client/src/app-controller.ts b/demo/client/src/app-controller.ts index d7c18fe4..fc7390d6 100644 --- a/demo/client/src/app-controller.ts +++ b/demo/client/src/app-controller.ts @@ -8,7 +8,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -import {AntialiasingStrategyName} from "./aa-strategy"; +import { AntialiasingStrategyName, SubpixelAAType } from "./aa-strategy"; import {FilePickerView} from "./file-picker"; import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader'; import {expectNotNull, unwrapNull, unwrapUndef} from './utils'; @@ -56,7 +56,7 @@ export abstract class DemoAppController extends protected shaderSources: ShaderMap | null; private aaLevelSelect: HTMLSelectElement | null; - private subpixelAASwitch: HTMLInputElement | null; + private subpixelAASelect: HTMLSelectElement | null; private fpsLabel: HTMLElement | null; constructor() { @@ -143,10 +143,10 @@ export abstract class DemoAppController extends if (this.aaLevelSelect != null) this.aaLevelSelect.addEventListener('change', () => this.updateAALevel(), false); - this.subpixelAASwitch = - document.getElementById('pf-subpixel-aa') as HTMLInputElement | null; - if (this.subpixelAASwitch != null) - this.subpixelAASwitch.addEventListener('change', () => this.updateAALevel(), false); + this.subpixelAASelect = + document.getElementById('pf-subpixel-aa-select') as HTMLSelectElement | null; + if (this.subpixelAASelect != null) + this.subpixelAASelect.addEventListener('change', () => this.updateAALevel(), false); this.updateAALevel(); } @@ -191,7 +191,11 @@ export abstract class DemoAppController extends aaLevel = 0; } - const subpixelAA = this.subpixelAASwitch == null ? false : this.subpixelAASwitch.checked; + let subpixelAA: SubpixelAAType; + if (this.subpixelAASelect != null) + subpixelAA = this.subpixelAASelect.selectedOptions[0].value as SubpixelAAType; + else + subpixelAA = 'none'; this.view.then(view => view.setAntialiasingOptions(aaType, aaLevel, subpixelAA)); } diff --git a/demo/client/src/benchmark.ts b/demo/client/src/benchmark.ts index 6eb16aa4..a400588b 100644 --- a/demo/client/src/benchmark.ts +++ b/demo/client/src/benchmark.ts @@ -12,6 +12,7 @@ import * as glmatrix from 'gl-matrix'; import * as opentype from "opentype.js"; import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; +import {SubpixelAAType} from "./aa-strategy"; import {AppController, DemoAppController} from "./app-controller"; import PathfinderBufferTexture from './buffer-texture'; import {OrthographicCamera} from './camera'; @@ -221,7 +222,7 @@ class BenchmarkTestView extends MonochromePathfinderView { protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean): + subpixelAA: SubpixelAAType): AntialiasingStrategy { return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); } diff --git a/demo/client/src/ecaa-strategy.ts b/demo/client/src/ecaa-strategy.ts index 9da89593..f2034f86 100644 --- a/demo/client/src/ecaa-strategy.ts +++ b/demo/client/src/ecaa-strategy.ts @@ -10,7 +10,7 @@ import * as glmatrix from 'gl-matrix'; -import {AntialiasingStrategy} from './aa-strategy'; +import { AntialiasingStrategy, SubpixelAAType } from './aa-strategy'; import PathfinderBufferTexture from './buffer-texture'; import {createFramebuffer, createFramebufferColorTexture} from './gl-utils'; import {createFramebufferDepthTexture, setTextureParameters, UniformMap} from './gl-utils'; @@ -35,7 +35,7 @@ export abstract class ECAAStrategy extends AntialiasingStrategy { protected supersampledFramebufferSize: glmatrix.vec2; protected destFramebufferSize: glmatrix.vec2; - protected subpixelAA: boolean; + protected subpixelAA: SubpixelAAType; private bVertexPositionBufferTexture: PathfinderBufferTexture; private bVertexPathIDBufferTexture: PathfinderBufferTexture; @@ -47,7 +47,7 @@ export abstract class ECAAStrategy extends AntialiasingStrategy { private curveVAOs: UpperAndLower; private resolveVAO: WebGLVertexArrayObject; - constructor(level: number, subpixelAA: boolean) { + constructor(level: number, subpixelAA: SubpixelAAType) { super(); this.subpixelAA = subpixelAA; @@ -472,13 +472,13 @@ export abstract class ECAAStrategy extends AntialiasingStrategy { } protected get supersampleScale(): glmatrix.vec2 { - return glmatrix.vec2.fromValues(this.subpixelAA ? 3.0 : 1.0, 1.0); + return glmatrix.vec2.fromValues(this.subpixelAA !== 'none' ? 3.0 : 1.0, 1.0); } } export class ECAAMonochromeStrategy extends ECAAStrategy { protected getResolveProgram(view: MonochromePathfinderView): PathfinderShaderProgram { - if (this.subpixelAA) + if (this.subpixelAA !== 'none') return view.shaderPrograms.ecaaMonoSubpixelResolve; return view.shaderPrograms.ecaaMonoResolve; } diff --git a/demo/client/src/ssaa-strategy.ts b/demo/client/src/ssaa-strategy.ts index 89a58c2c..28c36d30 100644 --- a/demo/client/src/ssaa-strategy.ts +++ b/demo/client/src/ssaa-strategy.ts @@ -10,14 +10,14 @@ import * as glmatrix from 'gl-matrix'; -import {AntialiasingStrategy} from './aa-strategy'; +import {AntialiasingStrategy, SubpixelAAType} from './aa-strategy'; import {createFramebuffer, createFramebufferDepthTexture, setTextureParameters} from './gl-utils'; import {unwrapNull} from './utils'; import {PathfinderDemoView} from './view'; export default class SSAAStrategy extends AntialiasingStrategy { private level: number; - private subpixelAA: boolean; + private subpixelAA: SubpixelAAType; private destFramebufferSize: glmatrix.vec2; private supersampledFramebufferSize: glmatrix.vec2; @@ -25,7 +25,7 @@ export default class SSAAStrategy extends AntialiasingStrategy { private supersampledDepthTexture: WebGLTexture; private supersampledFramebuffer: WebGLFramebuffer; - constructor(level: number, subpixelAA: boolean) { + constructor(level: number, subpixelAA: SubpixelAAType) { super(); this.level = level; this.subpixelAA = subpixelAA; @@ -96,7 +96,7 @@ export default class SSAAStrategy extends AntialiasingStrategy { // Set up the blit program VAO. let resolveProgram; - if (this.subpixelAA) + if (this.subpixelAA !== 'none') resolveProgram = view.shaderPrograms.ssaaSubpixelResolve; else resolveProgram = view.shaderPrograms.blit; @@ -120,7 +120,7 @@ export default class SSAAStrategy extends AntialiasingStrategy { } private get supersampleScale(): glmatrix.vec2 { - return glmatrix.vec2.fromValues(this.subpixelAA ? 3 : 2, this.level === 2 ? 1 : 2); + return glmatrix.vec2.clone([this.subpixelAA !== 'none' ? 3 : 2, this.level === 2 ? 1 : 2]); } private usedSupersampledFramebufferSize(view: PathfinderDemoView): glmatrix.vec2 { diff --git a/demo/client/src/svg-demo.ts b/demo/client/src/svg-demo.ts index c2147a48..4e968c01 100644 --- a/demo/client/src/svg-demo.ts +++ b/demo/client/src/svg-demo.ts @@ -11,7 +11,7 @@ import * as glmatrix from 'gl-matrix'; import * as _ from 'lodash'; -import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; +import { AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy, SubpixelAAType } from "./aa-strategy"; import {DemoAppController} from './app-controller'; import PathfinderBufferTexture from "./buffer-texture"; import {OrthographicCamera} from "./camera"; @@ -153,7 +153,7 @@ class SVGDemoView extends PathfinderDemoView { protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean): + subpixelAA: SubpixelAAType): AntialiasingStrategy { return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); } diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index 70b143b6..0053ca8e 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -14,7 +14,8 @@ import * as _ from 'lodash'; import * as opentype from 'opentype.js'; import {Metrics} from 'opentype.js'; -import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy'; +import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; +import {SubpixelAAType} from './aa-strategy'; import {DemoAppController} from './app-controller'; import PathfinderBufferTexture from './buffer-texture'; import {OrthographicCamera} from "./camera"; @@ -71,6 +72,11 @@ const INITIAL_FONT_SIZE: number = 72.0; const DEFAULT_FONT: string = 'open-sans'; +const FONT_DILATION_FACTOR: number = 1.0242; + +/// In pixels. +const MAX_FONT_DILATION: number = 0.3; + const B_POSITION_SIZE: number = 8; const B_PATH_INDEX_SIZE: number = 2; @@ -172,10 +178,6 @@ class TextDemoController extends DemoAppController { window.jQuery(this.editTextModal).modal(); } - createHint(): Hint { - return new Hint(this.font, this.pixelsPerUnit, this.useHinting); - } - protected createView() { return new TextDemoView(this, unwrapNull(this.commonShaderSource), @@ -245,7 +247,7 @@ class TextDemoController extends DemoAppController { this.view.then(view => view.relayoutText()); } - get pixelsPerUnit(): number { + get layoutPixelsPerUnit(): number { return this._fontSize / this.font.opentypeFont.unitsPerEm; } @@ -283,6 +285,8 @@ class TextDemoView extends MonochromePathfinderView { protected depthFunction: number = this.gl.GREATER; + private subpixelAA: SubpixelAAType; + constructor(appController: TextDemoController, commonShaderSource: string, shaderSources: ShaderMap) { @@ -327,6 +331,17 @@ class TextDemoView extends MonochromePathfinderView { this.setDirty(); } + setAntialiasingOptions(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType) { + super.setAntialiasingOptions(aaType, aaLevel, subpixelAA); + + // Need to relayout because changing AA options can cause font dilation to change... + this.layoutText(); + this.buildAtlasGlyphs(); + this.setDirty(); + } + protected initContext() { super.initContext(); } @@ -348,7 +363,7 @@ class TextDemoView extends MonochromePathfinderView { protected pathTransformsForObject(objectIndex: number): Float32Array { const pathCount = this.appController.pathCount; const atlasGlyphs = this.appController.atlasGlyphs; - const pixelsPerUnit = this.appController.pixelsPerUnit; + const pixelsPerUnit = this.displayPixelsPerUnit; const transforms = new Float32Array((pathCount + 1) * 4); @@ -435,8 +450,9 @@ class TextDemoView extends MonochromePathfinderView { protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean): + subpixelAA: SubpixelAAType): AntialiasingStrategy { + this.subpixelAA = subpixelAA; return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); } @@ -444,6 +460,12 @@ class TextDemoView extends MonochromePathfinderView { this.appController.newTimingsReceived(this.lastTimings); } + private createHint(): Hint { + return new Hint(this.appController.font, + this.displayPixelsPerUnit, + this.appController.useHinting); + } + /// Lays out glyphs on the canvas. private layoutText() { const layout = this.appController.layout; @@ -456,8 +478,9 @@ class TextDemoView extends MonochromePathfinderView { const glyphPositions = new Float32Array(totalGlyphCount * 8); const glyphIndices = new Uint32Array(totalGlyphCount * 6); - const hint = this.appController.createHint(); - const pixelsPerUnit = this.appController.pixelsPerUnit; + const hint = this.createHint(); + const displayPixelsPerUnit = this.displayPixelsPerUnit; + const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; let globalGlyphIndex = 0; for (const run of layout.textFrame.runs) { @@ -465,7 +488,8 @@ class TextDemoView extends MonochromePathfinderView { glyphIndex < run.glyphIDs.length; glyphIndex++, globalGlyphIndex++) { const rect = run.pixelRectForGlyphAt(glyphIndex, - pixelsPerUnit, + layoutPixelsPerUnit, + displayPixelsPerUnit, hint, SUBPIXEL_GRANULARITY); glyphPositions.set([ @@ -495,10 +519,11 @@ class TextDemoView extends MonochromePathfinderView { private buildAtlasGlyphs() { const font = this.appController.font; const glyphStore = this.appController.glyphStore; - const pixelsPerUnit = this.appController.pixelsPerUnit; + const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + const displayPixelsPerUnit = this.displayPixelsPerUnit; const textFrame = this.appController.layout.textFrame; - const hint = this.appController.createHint(); + const hint = this.createHint(); // Only build glyphs in view. const translation = this.camera.translation; @@ -511,7 +536,8 @@ class TextDemoView extends MonochromePathfinderView { for (const run of textFrame.runs) { for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) { const pixelRect = run.pixelRectForGlyphAt(glyphIndex, - pixelsPerUnit, + layoutPixelsPerUnit, + displayPixelsPerUnit, hint, SUBPIXEL_GRANULARITY); if (!rectsIntersect(pixelRect, canvasRect)) @@ -523,7 +549,7 @@ class TextDemoView extends MonochromePathfinderView { continue; const subpixel = run.subpixelForGlyphAt(glyphIndex, - pixelsPerUnit, + layoutPixelsPerUnit, hint, SUBPIXEL_GRANULARITY); const glyphKey = new GlyphKey(glyphID, subpixel); @@ -537,7 +563,7 @@ class TextDemoView extends MonochromePathfinderView { return; this.appController.atlasGlyphs = atlasGlyphs; - this.appController.atlas.layoutGlyphs(atlasGlyphs, font, pixelsPerUnit, hint); + this.appController.atlas.layoutGlyphs(atlasGlyphs, font, displayPixelsPerUnit, hint); this.uploadPathTransforms(1); @@ -577,8 +603,9 @@ class TextDemoView extends MonochromePathfinderView { const font = this.appController.font; const atlasGlyphs = this.appController.atlasGlyphs; - const hint = this.appController.createHint(); - const pixelsPerUnit = this.appController.pixelsPerUnit; + const hint = this.createHint(); + const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + const displayPixelsPerUnit = this.displayPixelsPerUnit; const atlasGlyphKeys = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.sortKey); @@ -592,7 +619,7 @@ class TextDemoView extends MonochromePathfinderView { const textGlyphID = run.glyphIDs[glyphIndex]; const subpixel = run.subpixelForGlyphAt(glyphIndex, - pixelsPerUnit, + layoutPixelsPerUnit, hint, SUBPIXEL_GRANULARITY); @@ -608,10 +635,10 @@ class TextDemoView extends MonochromePathfinderView { if (atlasGlyphMetrics == null) continue; - const atlasGlyphPixelOrigin = atlasGlyph.calculateSubpixelOrigin(pixelsPerUnit); + const atlasGlyphPixelOrigin = atlasGlyph.calculateSubpixelOrigin(displayPixelsPerUnit); const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphMetrics, atlasGlyphPixelOrigin, - pixelsPerUnit, + displayPixelsPerUnit, hint); const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2; const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2; @@ -678,6 +705,18 @@ class TextDemoView extends MonochromePathfinderView { protected get directInteriorProgramName(): keyof ShaderMap { return 'directInterior'; } + + /// Pixels per unit, including dilation. + private get displayPixelsPerUnit(): number { + // FIXME(pcwalton): Check against Cocoa and make sure this is what they do. + const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + if (this.subpixelAA !== 'strong') + return layoutPixelsPerUnit; + + const ascender = this.appController.font.opentypeFont.ascender * layoutPixelsPerUnit; + const maxFontDilationFactor = (ascender + MAX_FONT_DILATION) / ascender; + return Math.min(maxFontDilationFactor, FONT_DILATION_FACTOR) * layoutPixelsPerUnit; + } } interface AntialiasingStrategyTable { diff --git a/demo/client/src/text.ts b/demo/client/src/text.ts index eb812ae5..1d026ba5 100644 --- a/demo/client/src/text.ts +++ b/demo/client/src/text.ts @@ -99,27 +99,32 @@ export class TextRun { } } - calculatePixelOriginForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint): + calculatePixelOriginForGlyphAt(index: number, + layoutPixelsPerUnit: number, + hint: Hint): glmatrix.vec2 { const textGlyphOrigin = glmatrix.vec2.clone(this.origin); textGlyphOrigin[0] += this.advances[index]; - glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit); + glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, layoutPixelsPerUnit); return textGlyphOrigin; } pixelRectForGlyphAt(index: number, - pixelsPerUnit: number, + layoutPixelsPerUnit: number, + displayPixelsPerUnit: number, hint: Hint, subpixelGranularity: number): glmatrix.vec4 { const metrics = unwrapNull(this.font.metricsForGlyph(this.glyphIDs[index])); - const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index, pixelsPerUnit, hint); + const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index, + layoutPixelsPerUnit, + hint); textGlyphOrigin[0] *= subpixelGranularity; glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin); textGlyphOrigin[0] /= subpixelGranularity; - return calculatePixelRectForGlyph(metrics, textGlyphOrigin, pixelsPerUnit, hint); + return calculatePixelRectForGlyph(metrics, textGlyphOrigin, displayPixelsPerUnit, hint); } subpixelForGlyphAt(index: number, diff --git a/demo/client/src/view.ts b/demo/client/src/view.ts index 1769ac2f..7c4d3741 100644 --- a/demo/client/src/view.ts +++ b/demo/client/src/view.ts @@ -10,7 +10,7 @@ import * as glmatrix from 'gl-matrix'; -import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; +import { AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy, SubpixelAAType } from "./aa-strategy"; import PathfinderBufferTexture from './buffer-texture'; import {Camera} from "./camera"; import {QUAD_ELEMENTS, UniformMap} from './gl-utils'; @@ -164,13 +164,13 @@ export abstract class PathfinderDemoView extends PathfinderView { this.wantsScreenshot = false; - this.antialiasingStrategy = new NoAAStrategy(0, false); + this.antialiasingStrategy = new NoAAStrategy(0, 'none'); this.antialiasingStrategy.init(this); } setAntialiasingOptions(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean) { + subpixelAA: SubpixelAAType) { this.antialiasingStrategy = this.createAAStrategy(aaType, aaLevel, subpixelAA); const canvas = this.canvas; @@ -385,7 +385,7 @@ export abstract class PathfinderDemoView extends PathfinderView { protected abstract createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, - subpixelAA: boolean): + subpixelAA: SubpixelAAType): AntialiasingStrategy; protected abstract compositeIfNecessary(): void;