Add an experimental implementation of macOS-like font dilation

Following Apple's earlier terminology, this is exposed as "strong"
subpixel AA.
This commit is contained in:
Patrick Walton 2017-09-29 22:12:09 -07:00
parent 99f6f2a104
commit 60ff71be84
11 changed files with 112 additions and 58 deletions

View File

@ -54,12 +54,14 @@
<option value="ecaa" selected>ECAA</option>
</select>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" value="" type="checkbox"
id="pf-subpixel-aa" checked>
Subpixel AA
</label>
<div class="form-group">
<label for="pf-subpixel-aa-select">Subpixel AA</label>
<select id="pf-subpixel-aa-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="medium" selected>Medium</option>
<option value="strong">Strong</option>
</select>
</div>
<div class="form-group">
<label for="pf-hinting-select">Hinting</label>

View File

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

View File

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

View File

@ -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<View extends PathfinderDemoView> extends
protected shaderSources: ShaderMap<ShaderProgramSource> | 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<View extends PathfinderDemoView> 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<View extends PathfinderDemoView> 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));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TextDemoView> {
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<TextDemoView> {
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<ShaderProgramSource>) {
@ -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<void> {
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 {

View File

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

View File

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