Skip to content
Snippets Groups Projects
Commit ebeff4fc authored by Alexander Rose's avatar Alexander Rose
Browse files

font-atlas improvements

parent b7cf4282
Branches
Tags
No related merge requests found
......@@ -6,53 +6,50 @@
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { edt } from 'mol-math/geometry/distance-transform';
import { createTextureImage } from 'mol-gl/renderable/util';
const TextAtlasCache: { [k: string]: TextAtlas } = {}
const TextAtlasCache: { [k: string]: FontAtlas } = {}
export function getTextAtlas (props: Partial<TextAtlasProps>) {
const hash = JSON.stringify(props)
if (TextAtlasCache[ hash ] === undefined) {
TextAtlasCache[ hash ] = new TextAtlas(props)
}
return TextAtlasCache[ hash ]
export function getFontAtlas (props: Partial<FontAtlasProps>) {
const hash = JSON.stringify(props)
if (TextAtlasCache[hash] === undefined) {
TextAtlasCache[hash] = new FontAtlas(props)
}
return TextAtlasCache[hash]
}
export type TextFonts = 'sans-serif' | 'monospace' | 'serif' | 'cursive'
export type TextStyles = 'normal' | 'italic' | 'oblique'
export type TextVariants = 'normal' | 'small-caps'
export type TextWeights = 'normal' | 'bold'
export type FontFamily = 'sans-serif' | 'monospace' | 'serif' | 'cursive'
export type FontStyle = 'normal' | 'italic' | 'oblique'
export type FontVariant = 'normal' | 'small-caps'
export type FontWeight = 'normal' | 'bold'
export const TextAtlasParams = {
fontFamily: PD.Select('sans-serif', [['sans-serif', 'Sans Serif'], ['monospace', 'Monospace'], ['serif', 'Serif'], ['cursive', 'Cursive']] as [TextFonts, string][]),
export const FontAtlasParams = {
fontFamily: PD.Select('sans-serif', [['sans-serif', 'Sans Serif'], ['monospace', 'Monospace'], ['serif', 'Serif'], ['cursive', 'Cursive']] as [FontFamily, string][]),
fontSize: PD.Numeric(36, { min: 4, max: 96, step: 1 }),
fontStyle: PD.Select('normal', [['normal', 'Normal'], ['italic', 'Italic'], ['oblique', 'Oblique']] as [TextStyles, string][]),
fontVariant: PD.Select('normal', [['normal', 'Normal'], ['small-caps', 'Small Caps']] as [TextVariants, string][]),
fontWeight: PD.Select('normal', [['normal', 'Normal'], ['bold', 'Bold']] as [TextWeights, string][]),
width: PD.Numeric(1024),
height: PD.Numeric(1024)
fontStyle: PD.Select('normal', [['normal', 'Normal'], ['italic', 'Italic'], ['oblique', 'Oblique']] as [FontStyle, string][]),
fontVariant: PD.Select('normal', [['normal', 'Normal'], ['small-caps', 'Small Caps']] as [FontVariant, string][]),
fontWeight: PD.Select('normal', [['normal', 'Normal'], ['bold', 'Bold']] as [FontWeight, string][]),
}
export type TextAtlasParams = typeof TextAtlasParams
export type TextAtlasProps = PD.Values<TextAtlasParams>
export type TextAtlasMap = { x: number, y: number, w: number, h: number }
export type FontAtlasParams = typeof FontAtlasParams
export type FontAtlasProps = PD.Values<FontAtlasParams>
export class TextAtlas {
readonly props: Readonly<TextAtlasProps>
readonly mapped: { [k: string]: TextAtlasMap } = {}
readonly placeholder: TextAtlasMap
readonly context: CanvasRenderingContext2D
export type FontAtlasMap = { x: number, y: number, w: number, h: number }
private canvas: HTMLCanvasElement
export class FontAtlas {
readonly props: Readonly<FontAtlasProps>
readonly mapped: { [k: string]: FontAtlasMap } = {}
readonly placeholder: FontAtlasMap
readonly texture = createTextureImage(4096 * 2048, 1)
private scratchW = 0
private scratchH = 0
private currentX = 0
private currentY = 0
private readonly scratchData: Uint8Array
private readonly cutoff = 0.25
private padding: number
private radius: number
private readonly cutoff = 0.5
readonly buffer: number
private readonly radius: number
private gridOuter: Float64Array
private gridInner: Float64Array
......@@ -64,17 +61,18 @@ export class TextAtlas {
private scratchCanvas: HTMLCanvasElement
private scratchContext: CanvasRenderingContext2D
private lineHeight: number
private maxWidth: number
private middle: number
readonly lineHeight: number
constructor (props: Partial<TextAtlasProps> = {}) {
const p = { ...PD.getDefaultValues(TextAtlasParams), ...props }
private readonly maxWidth: number
private readonly middle: number
constructor (props: Partial<FontAtlasProps> = {}) {
const p = { ...PD.getDefaultValues(FontAtlasParams), ...props }
this.props = p
this.padding = p.fontSize / 8
this.buffer = p.fontSize / 8
this.radius = p.fontSize / 3
this.lineHeight = Math.round(p.fontSize + 6 * this.padding)
this.lineHeight = Math.round(p.fontSize + 2 * this.buffer + this.radius)
this.maxWidth = this.lineHeight * 1.5
// Prepare scratch canvas
......@@ -87,6 +85,9 @@ export class TextAtlas {
this.scratchContext.fillStyle = 'black'
this.scratchContext.textBaseline = 'middle'
// SDF scratch values
this.scratchData = new Uint8Array(this.lineHeight * this.maxWidth)
// temporary arrays for the distance transform
this.gridOuter = new Float64Array(this.lineHeight * this.maxWidth)
this.gridInner = new Float64Array(this.lineHeight * this.maxWidth)
......@@ -97,83 +98,56 @@ export class TextAtlas {
this.middle = Math.ceil(this.lineHeight / 2)
//
this.canvas = document.createElement('canvas')
this.canvas.width = p.width
this.canvas.height = p.height
this.context = this.canvas.getContext('2d')!
// Replacement Character
this.placeholder = this.map(String.fromCharCode(0xFFFD))
// Basic Latin (subset)
for (let i = 0x0020; i <= 0x007E; ++i) this.map(String.fromCharCode(i))
// TODO: to slow to always prepare them
// // Latin-1 Supplement (subset)
// for (let i = 0x00A1; i <= 0x00FF; ++i) this.map(String.fromCharCode(i))
// Degree sign
this.map(String.fromCharCode(0x00B0))
// // Greek and Coptic (subset)
// for (let i = 0x0391; i <= 0x03C9; ++i) this.map(String.fromCharCode(i))
// // Cyrillic (subset)
// for (let i = 0x0400; i <= 0x044F; ++i) this.map(String.fromCharCode(i))
// Angstrom Sign
this.map(String.fromCharCode(0x212B))
this.placeholder = this.get(String.fromCharCode(0xFFFD))
}
map (text: string) {
if (this.mapped[text] === undefined) {
this.draw(text)
get (char: string) {
if (this.mapped[char] === undefined) {
this.draw(char)
const { array, width, height } = this.texture
const data = this.scratchData
if (this.currentX + this.scratchW > this.props.width) {
if (this.currentX + this.scratchW > width) {
this.currentX = 0
this.currentY += this.scratchH
}
if (this.currentY + this.scratchH > this.props.height) {
if (this.currentY + this.scratchH > height) {
console.warn('canvas to small')
return this.placeholder
}
this.mapped[text] = {
this.mapped[char] = {
x: this.currentX, y: this.currentY,
w: this.scratchW, h: this.scratchH
}
this.context.drawImage(
this.scratchCanvas,
0, 0, this.scratchW, this.scratchH,
this.currentX, this.currentY, this.scratchW, this.scratchH
)
for (let y = 0; y < this.scratchH; ++y) {
for (let x = 0; x < this.scratchW; ++x) {
array[width * (this.currentY + y) + this.currentX + x] = data[y * this.scratchW + x]
}
}
this.currentX += this.scratchW
}
return this.mapped[text]
}
get (text: string) {
return this.mapped[text] || this.placeholder
return this.mapped[char]
}
draw (text: string) {
draw (char: string) {
const h = this.lineHeight
const ctx = this.scratchContext
const data = this.scratchData
// Measure text
const m = ctx.measureText(text)
const w = Math.min(this.maxWidth, Math.ceil(m.width + 2 * this.padding + this.radius / 2))
const m = ctx.measureText(char)
const w = Math.min(this.maxWidth, Math.ceil(m.width + 2 * this.buffer))
const n = w * h
ctx.clearRect(0, 0, w, h) // clear scratch area
ctx.fillText(text, this.padding + this.radius / 4, this.middle) // draw text
ctx.fillText(char, this.buffer, this.middle) // draw text
const imageData = ctx.getImageData(0, 0, w, h)
const data = imageData.data
for (let i = 0; i < n; i++) {
const a = imageData.data[i * 4 + 3] / 255 // alpha value
......@@ -186,10 +160,9 @@ export class TextAtlas {
for (let i = 0; i < n; i++) {
const d = this.gridOuter[i] - this.gridInner[i];
data[i * 4 + 3] = Math.max(0, Math.min(255, Math.round(255 - 255 * (d / this.radius + this.cutoff))));
data[i] = Math.max(0, Math.min(255, Math.round(255 - 255 * (d / this.radius + this.cutoff))))
}
ctx.putImageData(imageData, 0, 0)
this.scratchW = w
this.scratchH = h
}
......
......@@ -34,6 +34,24 @@ export function createTextureImage(n: number, itemSize: number): TextureImage<Ui
return { array: new Uint8Array(length), width, height }
}
export function printTextureImage(textureImage: TextureImage<any>, scale = 1) {
const { array, width, height } = textureImage
const itemSize = array.length / (width * height)
const data = new Uint8ClampedArray(width * height * 4)
if (itemSize === 1) {
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
data[(y * width + x) * 4 + 3] = array[y * width + x]
}
}
} else if (itemSize === 4) {
data.set(array)
} else {
console.warn(`itemSize '${itemSize}' not supported`)
}
return printImageData(new ImageData(data, width, height), scale)
}
export function printImageData(imageData: ImageData, scale = 1) {
const canvas = document.createElement('canvas')
canvas.width = imageData.width
......
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import './index.html'
import { FontAtlas } from 'mol-geo/geometry/text/font-atlas';
import { printTextureImage } from 'mol-gl/renderable/util';
function test() {
console.time('FontAtlas init')
const fontAtlas = new FontAtlas({ fontSize: 96 })
console.timeEnd('FontAtlas init')
console.time('Basic Latin (subset)')
for (let i = 0x0020; i <= 0x007E; ++i) fontAtlas.get(String.fromCharCode(i))
console.timeEnd('Basic Latin (subset)')
console.time('Latin-1 Supplement (subset)')
for (let i = 0x00A1; i <= 0x00FF; ++i) fontAtlas.get(String.fromCharCode(i))
console.timeEnd('Latin-1 Supplement (subset)')
console.time('Greek and Coptic (subset)')
for (let i = 0x0391; i <= 0x03C9; ++i) fontAtlas.get(String.fromCharCode(i))
console.timeEnd('Greek and Coptic (subset)')
console.time('Cyrillic (subset)')
for (let i = 0x0400; i <= 0x044F; ++i) fontAtlas.get(String.fromCharCode(i))
console.timeEnd('Cyrillic (subset)')
console.time('Angstrom Sign')
fontAtlas.get(String.fromCharCode(0x212B))
console.timeEnd('Angstrom Sign')
printTextureImage(fontAtlas.texture, 0.5)
}
test();
\ No newline at end of file
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import './index.html'
import { TextAtlas } from 'mol-geo/geometry/text/text-atlas';
import { printImageData } from 'mol-gl/renderable/util';
function test() {
console.time('TextAtlas')
const textAtlas = new TextAtlas()
console.timeEnd('TextAtlas')
const ctx = textAtlas.context
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
printImageData(imageData)
}
test();
\ No newline at end of file
......@@ -65,7 +65,7 @@ module.exports = [
createApp('viewer'),
createApp('model-server-query'),
createBrowserTest('text-atlas'),
createBrowserTest('font-atlas'),
createBrowserTest('render-text'),
createBrowserTest('render-spheres'),
createBrowserTest('render-mesh')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment