2017-10-17 14:40:45 -04:00
|
|
|
// pathfinder/client/src/text-renderer.ts
|
|
|
|
//
|
|
|
|
// Copyright © 2017 The Pathfinder Project Developers.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
|
|
// option. This file may not be copied, modified, or distributed
|
|
|
|
// except according to those terms.
|
|
|
|
|
|
|
|
import * as glmatrix from 'gl-matrix';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
|
|
|
|
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
|
|
|
|
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
|
|
|
|
import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './atlas';
|
2017-10-17 15:10:20 -04:00
|
|
|
import {CameraView, OrthographicCamera} from './camera';
|
2017-10-17 14:58:03 -04:00
|
|
|
import {createFramebuffer, createFramebufferDepthTexture, QUAD_ELEMENTS} from './gl-utils';
|
|
|
|
import {UniformMap} from './gl-utils';
|
2017-11-29 13:50:47 -05:00
|
|
|
import {PathTransformBuffers, Renderer} from './renderer';
|
2017-10-17 14:40:45 -04:00
|
|
|
import {ShaderMap} from './shader-loader';
|
|
|
|
import SSAAStrategy from './ssaa-strategy';
|
|
|
|
import {calculatePixelRectForGlyph, computeStemDarkeningAmount, GlyphStore, Hint} from "./text";
|
|
|
|
import {PathfinderFont, SimpleTextLayout, UnitMetrics} from "./text";
|
|
|
|
import {unwrapNull} from './utils';
|
|
|
|
import {RenderContext, Timings} from "./view";
|
|
|
|
import {AdaptiveMonochromeXCAAStrategy} from './xcaa-strategy';
|
|
|
|
|
|
|
|
interface AntialiasingStrategyTable {
|
|
|
|
none: typeof NoAAStrategy;
|
|
|
|
ssaa: typeof SSAAStrategy;
|
|
|
|
xcaa: typeof AdaptiveMonochromeXCAAStrategy;
|
|
|
|
}
|
|
|
|
|
2017-11-10 11:40:31 -05:00
|
|
|
const SQRT_1_2: number = 1.0 / Math.sqrt(2.0);
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
const MIN_SCALE: number = 0.0025;
|
|
|
|
const MAX_SCALE: number = 0.5;
|
|
|
|
|
|
|
|
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
|
|
|
none: NoAAStrategy,
|
|
|
|
ssaa: SSAAStrategy,
|
|
|
|
xcaa: AdaptiveMonochromeXCAAStrategy,
|
|
|
|
};
|
|
|
|
|
|
|
|
export interface TextRenderContext extends RenderContext {
|
|
|
|
atlasGlyphs: AtlasGlyph[];
|
|
|
|
|
2017-10-17 15:10:20 -04:00
|
|
|
readonly cameraView: CameraView;
|
2017-10-17 14:40:45 -04:00
|
|
|
readonly atlas: Atlas;
|
|
|
|
readonly glyphStore: GlyphStore;
|
|
|
|
readonly font: PathfinderFont;
|
|
|
|
readonly fontSize: number;
|
|
|
|
readonly useHinting: boolean;
|
|
|
|
|
|
|
|
newTimingsReceived(timings: Timings): void;
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
export abstract class TextRenderer extends Renderer {
|
2017-10-17 14:40:45 -04:00
|
|
|
renderContext: TextRenderContext;
|
|
|
|
|
|
|
|
camera: OrthographicCamera;
|
|
|
|
|
|
|
|
atlasFramebuffer: WebGLFramebuffer;
|
|
|
|
atlasDepthTexture: WebGLTexture;
|
|
|
|
|
|
|
|
get destFramebuffer(): WebGLFramebuffer {
|
|
|
|
return this.atlasFramebuffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
get destAllocatedSize(): glmatrix.vec2 {
|
|
|
|
return ATLAS_SIZE;
|
|
|
|
}
|
|
|
|
|
|
|
|
get destUsedSize(): glmatrix.vec2 {
|
|
|
|
return this.renderContext.atlas.usedSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
get emboldenAmount(): glmatrix.vec2 {
|
|
|
|
return this.stemDarkeningAmount;
|
|
|
|
}
|
|
|
|
|
|
|
|
get bgColor(): glmatrix.vec4 {
|
2017-10-26 23:15:41 -04:00
|
|
|
return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get fgColor(): glmatrix.vec4 {
|
2017-10-26 23:15:41 -04:00
|
|
|
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|
|
|
|
|
2017-11-29 13:50:47 -05:00
|
|
|
get rotationAngle(): number {
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
2017-11-30 12:51:07 -05:00
|
|
|
protected get pixelsPerUnit(): number {
|
2017-10-17 16:37:36 -04:00
|
|
|
return this.renderContext.fontSize / this.renderContext.font.opentypeFont.unitsPerEm;
|
|
|
|
}
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
protected get worldTransform(): glmatrix.mat4 {
|
|
|
|
const transform = glmatrix.mat4.create();
|
|
|
|
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
|
|
|
|
glmatrix.mat4.scale(transform, transform, [2.0 / ATLAS_SIZE[0], 2.0 / ATLAS_SIZE[1], 1.0]);
|
|
|
|
return transform;
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
protected get stemDarkeningAmount(): glmatrix.vec2 {
|
2017-11-30 12:51:07 -05:00
|
|
|
if (this.stemDarkening === 'dark')
|
|
|
|
return computeStemDarkeningAmount(this.renderContext.fontSize, this.pixelsPerUnit);
|
2017-10-17 16:37:36 -04:00
|
|
|
return glmatrix.vec2.create();
|
|
|
|
}
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
protected get usedSizeFactor(): glmatrix.vec2 {
|
|
|
|
const usedSize = glmatrix.vec2.create();
|
|
|
|
glmatrix.vec2.div(usedSize, this.renderContext.atlas.usedSize, ATLAS_SIZE);
|
|
|
|
return usedSize;
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
private get pathCount(): number {
|
|
|
|
return this.renderContext.glyphStore.glyphIDs.length * SUBPIXEL_GRANULARITY;
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|
|
|
|
|
2017-11-01 19:09:58 -04:00
|
|
|
protected get objectCount(): number {
|
2017-11-15 17:36:59 -05:00
|
|
|
return this.meshes == null ? 0 : this.meshes.length;
|
2017-11-01 19:09:58 -04:00
|
|
|
}
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
private stemDarkening: StemDarkeningMode;
|
|
|
|
private subpixelAA: SubpixelAAType;
|
|
|
|
|
|
|
|
constructor(renderContext: TextRenderContext) {
|
|
|
|
super(renderContext);
|
|
|
|
|
2017-10-17 15:10:20 -04:00
|
|
|
this.camera = new OrthographicCamera(this.renderContext.cameraView, {
|
2017-10-17 14:40:45 -04:00
|
|
|
maxScale: MAX_SCALE,
|
|
|
|
minScale: MIN_SCALE,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
setHintsUniform(uniforms: UniformMap): void {
|
|
|
|
const hint = this.createHint();
|
|
|
|
this.renderContext.gl.uniform4f(uniforms.uHints,
|
|
|
|
hint.xHeight,
|
|
|
|
hint.hintedXHeight,
|
|
|
|
hint.stemHeight,
|
|
|
|
hint.hintedStemHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
pathBoundingRects(objectIndex: number): Float32Array {
|
2017-10-17 16:37:36 -04:00
|
|
|
const pathCount = this.pathCount;
|
2017-10-17 14:40:45 -04:00
|
|
|
const atlasGlyphs = this.renderContext.atlasGlyphs;
|
2017-11-30 12:51:07 -05:00
|
|
|
const pixelsPerUnit = this.pixelsPerUnit;
|
2017-10-17 14:40:45 -04:00
|
|
|
const font = this.renderContext.font;
|
|
|
|
const hint = this.createHint();
|
|
|
|
|
|
|
|
const boundingRects = new Float32Array((pathCount + 1) * 4);
|
|
|
|
|
|
|
|
for (const glyph of atlasGlyphs) {
|
|
|
|
const atlasGlyphMetrics = font.metricsForGlyph(glyph.glyphKey.id);
|
|
|
|
if (atlasGlyphMetrics == null)
|
|
|
|
continue;
|
|
|
|
const atlasUnitMetrics = new UnitMetrics(atlasGlyphMetrics, this.stemDarkeningAmount);
|
|
|
|
|
|
|
|
const pathID = glyph.pathID;
|
|
|
|
boundingRects[pathID * 4 + 0] = atlasUnitMetrics.left;
|
|
|
|
boundingRects[pathID * 4 + 1] = atlasUnitMetrics.descent;
|
|
|
|
boundingRects[pathID * 4 + 2] = atlasUnitMetrics.right;
|
|
|
|
boundingRects[pathID * 4 + 3] = atlasUnitMetrics.ascent;
|
|
|
|
}
|
|
|
|
|
|
|
|
return boundingRects;
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
protected createAtlasFramebuffer(): void {
|
|
|
|
const atlasColorTexture = this.renderContext.atlas.ensureTexture(this.renderContext);
|
|
|
|
this.atlasDepthTexture = createFramebufferDepthTexture(this.renderContext.gl, ATLAS_SIZE);
|
|
|
|
this.atlasFramebuffer = createFramebuffer(this.renderContext.gl,
|
2017-10-21 01:04:53 -04:00
|
|
|
atlasColorTexture,
|
2017-10-17 16:37:36 -04:00
|
|
|
this.atlasDepthTexture);
|
|
|
|
|
|
|
|
// Allow the antialiasing strategy to set up framebuffers as necessary.
|
|
|
|
if (this.antialiasingStrategy != null)
|
|
|
|
this.antialiasingStrategy.setFramebufferSize(this);
|
|
|
|
}
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
protected createAAStrategy(aaType: AntialiasingStrategyName,
|
|
|
|
aaLevel: number,
|
|
|
|
subpixelAA: SubpixelAAType,
|
|
|
|
stemDarkening: StemDarkeningMode):
|
|
|
|
AntialiasingStrategy {
|
|
|
|
this.subpixelAA = subpixelAA;
|
|
|
|
this.stemDarkening = stemDarkening;
|
|
|
|
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
|
|
|
|
}
|
|
|
|
|
2017-11-15 17:36:59 -05:00
|
|
|
protected clearForDirectRendering(): void {}
|
2017-10-17 14:40:45 -04:00
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
protected buildAtlasGlyphs(atlasGlyphs: AtlasGlyph[]): void {
|
|
|
|
const font = this.renderContext.font;
|
2017-11-30 12:51:07 -05:00
|
|
|
const pixelsPerUnit = this.pixelsPerUnit;
|
2017-10-17 16:37:36 -04:00
|
|
|
const hint = this.createHint();
|
|
|
|
|
|
|
|
atlasGlyphs.sort((a, b) => a.glyphKey.sortKey - b.glyphKey.sortKey);
|
|
|
|
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.glyphKey.sortKey);
|
|
|
|
if (atlasGlyphs.length === 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.renderContext.atlasGlyphs = atlasGlyphs;
|
|
|
|
this.renderContext.atlas.layoutGlyphs(atlasGlyphs,
|
|
|
|
font,
|
2017-11-30 12:51:07 -05:00
|
|
|
pixelsPerUnit,
|
2017-10-17 16:37:36 -04:00
|
|
|
hint,
|
|
|
|
this.stemDarkeningAmount);
|
|
|
|
|
|
|
|
this.uploadPathTransforms(1);
|
2017-10-17 18:30:33 -04:00
|
|
|
this.uploadPathColors(1);
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
protected pathColorsForObject(objectIndex: number): Uint8Array {
|
2017-10-17 16:37:36 -04:00
|
|
|
const pathCount = this.pathCount;
|
2017-10-17 14:40:45 -04:00
|
|
|
|
|
|
|
const pathColors = new Uint8Array(4 * (pathCount + 1));
|
|
|
|
|
|
|
|
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
|
|
|
for (let channel = 0; channel < 3; channel++)
|
2017-10-26 23:15:41 -04:00
|
|
|
pathColors[(pathIndex + 1) * 4 + channel] = 0xff; // RGB
|
2017-10-17 14:40:45 -04:00
|
|
|
pathColors[(pathIndex + 1) * 4 + 3] = 0xff; // alpha
|
|
|
|
}
|
|
|
|
|
|
|
|
return pathColors;
|
|
|
|
}
|
|
|
|
|
2017-11-29 13:50:47 -05:00
|
|
|
protected pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
|
2017-10-17 16:37:36 -04:00
|
|
|
const pathCount = this.pathCount;
|
2017-10-17 14:40:45 -04:00
|
|
|
const atlasGlyphs = this.renderContext.atlasGlyphs;
|
2017-11-30 12:51:07 -05:00
|
|
|
const pixelsPerUnit = this.pixelsPerUnit;
|
2017-11-29 13:50:47 -05:00
|
|
|
const rotationAngle = this.rotationAngle;
|
2017-10-17 14:40:45 -04:00
|
|
|
|
2017-11-10 11:40:31 -05:00
|
|
|
// FIXME(pcwalton): This is a hack that tries to preserve the vertical extents of the glyph
|
|
|
|
// after stem darkening. It's better than nothing, but we should really do better.
|
|
|
|
//
|
|
|
|
// This hack seems to produce *better* results than what macOS does on sans-serif fonts;
|
|
|
|
// the ascenders and x-heights of the glyphs are pixel snapped, while they aren't on macOS.
|
|
|
|
// But we should really figure out what macOS does…
|
|
|
|
const ascender = this.renderContext.font.opentypeFont.ascender;
|
|
|
|
const stemDarkeningAmount = this.stemDarkeningAmount;
|
|
|
|
const stemDarkeningYScale = (ascender + stemDarkeningAmount[1]) / ascender;
|
|
|
|
|
|
|
|
const stemDarkeningOffset = glmatrix.vec2.clone(stemDarkeningAmount);
|
2017-11-10 11:16:27 -05:00
|
|
|
glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, pixelsPerUnit);
|
2017-11-10 11:40:31 -05:00
|
|
|
glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, SQRT_1_2);
|
|
|
|
glmatrix.vec2.mul(stemDarkeningOffset, stemDarkeningOffset, [1, stemDarkeningYScale]);
|
2017-11-10 11:16:27 -05:00
|
|
|
|
2017-11-29 13:50:47 -05:00
|
|
|
const transform = glmatrix.mat2d.create();
|
|
|
|
const transforms = this.createPathTransformBuffers(pathCount);
|
2017-10-17 14:40:45 -04:00
|
|
|
|
|
|
|
for (const glyph of atlasGlyphs) {
|
|
|
|
const pathID = glyph.pathID;
|
|
|
|
const atlasOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit);
|
|
|
|
|
2017-11-29 13:50:47 -05:00
|
|
|
glmatrix.mat2d.identity(transform);
|
|
|
|
glmatrix.mat2d.translate(transform, transform, atlasOrigin);
|
|
|
|
glmatrix.mat2d.translate(transform, transform, stemDarkeningOffset);
|
|
|
|
glmatrix.mat2d.rotate(transform, transform, rotationAngle);
|
|
|
|
glmatrix.mat2d.scale(transform,
|
|
|
|
transform,
|
|
|
|
[pixelsPerUnit, pixelsPerUnit * stemDarkeningYScale]);
|
|
|
|
|
|
|
|
transforms.st[pathID * 4 + 0] = transform[0];
|
|
|
|
transforms.st[pathID * 4 + 1] = transform[3];
|
|
|
|
transforms.st[pathID * 4 + 2] = transform[4];
|
|
|
|
transforms.st[pathID * 4 + 3] = transform[5];
|
|
|
|
|
|
|
|
transforms.ext[pathID * 2 + 0] = transform[1];
|
|
|
|
transforms.ext[pathID * 2 + 1] = transform[2];
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return transforms;
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
protected newTimingsReceived(): void {
|
2017-10-17 14:40:45 -04:00
|
|
|
this.renderContext.newTimingsReceived(this.lastTimings);
|
|
|
|
}
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
protected createHint(): Hint {
|
2017-10-17 14:40:45 -04:00
|
|
|
return new Hint(this.renderContext.font,
|
2017-11-30 12:51:07 -05:00
|
|
|
this.pixelsPerUnit,
|
2017-10-17 14:40:45 -04:00
|
|
|
this.renderContext.useHinting);
|
|
|
|
}
|
2017-10-30 16:34:55 -04:00
|
|
|
|
2017-10-31 15:41:38 -04:00
|
|
|
protected directCurveProgramName(): keyof ShaderMap<void> {
|
2017-10-30 16:34:55 -04:00
|
|
|
return 'directCurve';
|
|
|
|
}
|
|
|
|
|
2017-10-31 15:41:38 -04:00
|
|
|
protected directInteriorProgramName(): keyof ShaderMap<void> {
|
2017-10-30 16:34:55 -04:00
|
|
|
return 'directInterior';
|
|
|
|
}
|
2017-10-17 14:40:45 -04:00
|
|
|
}
|