Alexander Rose пре 6 година
родитељ
комит
ebeff4fc6e

+ 63 - 90
src/mol-geo/geometry/text/text-atlas.ts → src/mol-geo/geometry/text/font-atlas.ts

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

+ 18 - 0
src/mol-gl/renderable/util.ts

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

+ 39 - 0
src/tests/browser/font-atlas.ts

@@ -0,0 +1,39 @@
+/**
+ * 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();

+ 0 - 20
src/tests/browser/text-atlas.ts

@@ -1,20 +0,0 @@
-/**
- * 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();

+ 1 - 1
webpack.config.js

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