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