import { FontJSON } from "*.font.json" ; import { loadImage } from "./graphics.js" ; const CHAR_SPACING = 1 ; export class BitmapFont { private sourceImage : HTMLImageElement ; private canvas : HTMLCanvasElement ; private colorMap : Record < string , number > ; private charMap : string ; private charWidths : number [ ] ; private compactablePrecursors : string [ ] [ ] ; private charStartPoints : number [ ] ; private charCount : number ; private charReverseMap : Record < string , number > ; public charHeight ! : number ; private constructor ( sourceImage , colors , charMap , charWidths , compactablePrecursors , charMargin = 1 ) { this . sourceImage = sourceImage ; this . canvas = document . createElement ( "canvas" ) ; this . colorMap = this . prepareColors ( colors ) ; this . charMap = charMap ; this . charWidths = charWidths ; this . compactablePrecursors = compactablePrecursors ; this . charStartPoints = [ ] ; this . charCount = charMap . length ; this . charReverseMap = { } ; for ( let i = 0 ; i < this . charCount ; i ++ ) { this . charStartPoints [ i ] = ( i === 0 ) ? 0 : this . charStartPoints [ i - 1 ] + this . charWidths [ i - 1 ] + charMargin ; const char = this . charMap [ i ] ; this . charReverseMap [ char ] = i ; } } param source return public static async load ( source ) { const json = await ( await fetch ( source ) ) . json ( ) as FontJSON ; const baseURL = new URL ( source , location . href ) ; const image = await loadImage ( new URL ( json . image , baseURL ) ) ; const characters = json . characterMapping . map ( charDef => charDef . char ) . join ( "" ) ; const widths = json . characterMapping . map ( charDef => charDef . width ) ; const compactablePrecursors = json . characterMapping . map ( charDef => charDef . compactablePrecursors || [ ] ) ; return new BitmapFont ( image , json . colors , characters , widths , compactablePrecursors , json . margin ) ; } private prepareColors ( colorMap ) { const result = { } ; const colors = Object . keys ( colorMap ) ; const count = colors . length ; const w = this . canvas . width = this . sourceImage . width ; const h = this . sourceImage . height ; this . canvas . height = h * count ; this . charHeight = h ; const ctx = this . canvas . getContext ( "2d" ) ! ; for ( let i = 0 ; i < count ; i ++ ) { result [ colors [ i ] ] = i ; ctx . drawImage ( this . sourceImage , 0 , h * i ) ; } ctx . globalCompositeOperation = "source-in" ; for ( let i = 0 ; i < count ; i ++ ) { ctx . fillStyle = colorMap [ colors [ i ] ] ; ctx . save ( ) ; ctx . beginPath ( ) ; ctx . rect ( 0 , h * i , w , h ) ; ctx . clip ( ) ; ctx . fillRect ( 0 , 0 , w , h * count ) ; ctx . restore ( ) ; } ctx . globalCompositeOperation = "source-over" ; return result ; } private getCharIndex ( char ) { let charIndex = this . charReverseMap [ char ] ; if ( charIndex == null ) { charIndex = this . charCount - 1 ; } return charIndex ; } private drawCharacter ( ctx , char , color ) { const colorIndex = this . colorMap [ color ] ; const charIndex = ( typeof char === "number" ) ? char : this . getCharIndex ( char ) ; const charX = this . charStartPoints [ charIndex ] , charY = colorIndex * this . charHeight ; ctx . drawImage ( this . canvas , charX , charY , this . charWidths [ charIndex ] , this . charHeight , 0 , 0 , this . charWidths [ charIndex ] , this . charHeight ) ; } public drawText ( ctx , text , x , y , color , align = 0 , alpha = 1 ) { ctx . save ( ) ; ctx . translate ( x , y ) ; if ( ctx . getTransform ) { const transform = ctx . getTransform ( ) ; ctx . translate ( Math . round ( transform . e ) - transform . e , Math . round ( transform . f ) - transform . f ) ; } text = "" + text ; ctx . globalAlpha *= alpha ; const { width } = this . measureText ( text ) ; ctx . translate ( - align * width , 0 ) ; let precursorChar = null ; for ( const currentChar of text ) { const index = this . getCharIndex ( currentChar ) ; const spaceReduction = precursorChar && this . compactablePrecursors [ index ] . includes ( precursorChar ) ? 1 : 0 ; ctx . translate ( - spaceReduction , 0 ) ; this . drawCharacter ( ctx , index , color ) ; ctx . translate ( this . charWidths [ index ] + CHAR_SPACING , 0 ) ; precursorChar = currentChar ; } ctx . restore ( ) ; } public measureText ( text ) { let width = 0 ; let precursorChar = null ; for ( const currentChar of text ) { const index = this . getCharIndex ( currentChar ) ; const spaceReduction = precursorChar && this . compactablePrecursors [ index ] . includes ( precursorChar ) ? 1 : 0 ; width += this . charWidths [ index ] - spaceReduction + CHAR_SPACING ; precursorChar = currentChar ; } if ( text . length > 0 ) { width -= CHAR_SPACING ; } return { width , height : this . charHeight } ; } public drawTextWithOutline ( ctx , text , xPos , yPos , textColor , outlineColor , align = 0 ) { for ( let yOffset = yPos - 1 ; yOffset <= yPos + 1 ; yOffset ++ ) { for ( let xOffset = xPos - 1 ; xOffset <= xPos + 1 ; xOffset ++ ) { if ( xOffset !== xPos || yOffset !== yPos ) { this . drawText ( ctx , text , xOffset , yOffset , outlineColor , align ) ; } } } this . drawText ( ctx , text , xPos , yPos , textColor , align ) ; } }