import { Graphics , WebGL } from "./graphics" ; import * as shaders from "./shaders" ; type JumpFloodQuality = "JFA" | "JFA+1" | "JFA+2" ; type Options = { outputCanvas ? : HTMLCanvasElement ; sizeHint ? : [ number , number ] ; } ; export class DistanceFieldGenerator { private _gl : WebGL . Context ; private _outputCanvas : HTMLCanvasElement ; private _prepJumpFloodData : Graphics . Material ; private _jumpFloodOutputSeedPosition : Graphics . Material ; private _jumpFloodOutputDistance : Graphics . Material ; private _quadBuffer : Graphics . VertexBuffer ; private _sourceTexture : Graphics . Texture | null = null ; private _destTexture : Graphics . Texture | null = null ; private _sourceTextureTarget : Graphics . RenderTarget | null = null ; private _destTextureTarget : Graphics . RenderTarget | null = null ; private _seedInputTexture : Graphics . Texture | null = null ; constructor ( options = { } ) { if ( options . outputCanvas != null ) { this . _outputCanvas = options . outputCanvas ; } else { this . _outputCanvas = document . createElement ( "canvas" ) as HTMLCanvasElement ; } const sizeHint = options . outputCanvas != null ? [ options . outputCanvas . width , options . outputCanvas . height ] : options . sizeHint != null ? options . sizeHint : [ 100 , 100 ] ; this . _gl = new WebGL . Context ( this . _outputCanvas ) ; this . _resizeOutputCanvasAndTextures ( sizeHint [ 0 ] , sizeHint [ 1 ] , "force-update" ) ; const gl = this . _gl ; this . _gl . setCopyBlendState ( ) ; const vertexFormat = new Graphics . VertexFormat ( ) ; vertexFormat . add ( shaders . GLSLX_NAME_A_QUAD , Graphics . AttributeType . FLOAT , 2 ) ; this . _prepJumpFloodData = gl . createMaterial ( vertexFormat , shaders . GLSLX_SOURCE_V_COPY_POSITION , shaders . GLSLX_SOURCE_F_PREP_FOR_JFA ) ; this . _jumpFloodOutputSeedPosition = gl . createMaterial ( vertexFormat , shaders . GLSLX_SOURCE_V_COPY_POSITION , shaders . GLSLX_SOURCE_F_JUMP_FLOOD_OUTPUT_SEED_POSITION ) ; this . _jumpFloodOutputDistance = gl . createMaterial ( vertexFormat , shaders . GLSLX_SOURCE_V_COPY_POSITION , shaders . GLSLX_SOURCE_F_JUMP_FLOOD_OUTPUT_DISTANCE ) ; const QUAD = new Float32Array ( [ - 1 , - 1 , 1 , - 1 , - 1 , 1 , 1 , 1 ] ) ; this . _quadBuffer = gl . createVertexBuffer ( vertexFormat . stride * QUAD . length ) ; this . _quadBuffer . upload ( new Uint8Array ( QUAD . buffer ) ) ; } destroy ( ) { if ( this . _sourceTexture ) { this . _sourceTexture . free ( ) ; } if ( this . _destTexture ) { this . _destTexture . free ( ) ; } if ( this . _seedInputTexture ) { this . _seedInputTexture . free ( ) ; } this . _quadBuffer . free ( ) ; this . _prepJumpFloodData . free ( ) ; this . _jumpFloodOutputDistance . free ( ) ; this . _jumpFloodOutputSeedPosition . free ( ) ; } public outputCanvas ( ) { return this . _outputCanvas ; } private _resizeTextureIfNecessary ( texture , newWidth , newHeight , forceUpdate = "dont-force" ) { if ( texture . width === newWidth && texture . height === newHeight && forceUpdate !== "force-update" ) { return ; } texture . resize ( newWidth , newHeight ) ; } private _resizeOutputCanvasAndTextures ( newWidth , newHeight , forceUpdate = "dont-force" ) { const gl = this . _gl ; const outputCanvas = this . _outputCanvas ; const outputCanvasSizeNeedsUpdate = outputCanvas . width !== newWidth || outputCanvas . height !== newHeight || forceUpdate === "force-update" ; if ( outputCanvasSizeNeedsUpdate ) { gl . resize ( newWidth , newHeight , newWidth , newHeight ) ; } if ( this . _sourceTexture == null ) { this . _sourceTexture = gl . createTexture ( Graphics . TextureFormat . NEAREST_CLAMP , newWidth , newHeight ) ; this . _sourceTextureTarget = gl . createRenderTarget ( this . _sourceTexture ) ; } else { this . _resizeTextureIfNecessary ( this . _sourceTexture , newWidth , newHeight ) ; } if ( this . _destTexture == null ) { this . _destTexture = gl . createTexture ( Graphics . TextureFormat . NEAREST_CLAMP , newWidth , newHeight ) ; this . _destTextureTarget = gl . createRenderTarget ( this . _destTexture ) ; } else { this . _resizeTextureIfNecessary ( this . _destTexture , newWidth , newHeight ) ; } if ( this . _seedInputTexture == null ) { this . _seedInputTexture = gl . createTexture ( Graphics . TextureFormat . NEAREST_CLAMP , newWidth , newHeight ) ; } else { this . _resizeTextureIfNecessary ( this . _seedInputTexture , newWidth , newHeight ) ; } } public generateSDF ( inputCanvas , quality = "JFA" ) { const width = inputCanvas . width ; const height = inputCanvas . height ; this . _resizeOutputCanvasAndTextures ( width , height ) ; this . _setSeedsFromCanvas ( inputCanvas ) ; const maxDimension = Math . max ( width , height ) ; let stepSize = nextPowerOfTwo ( maxDimension ) / 2 ; while ( stepSize >= 1 ) { const isLastStep = stepSize / 2 < 1 && quality == "JFA" ; const output = isLastStep ? JumpFloodOutput . FOR_SCREEN : JumpFloodOutput . FOR_ANOTHER_STEP ; this . _runJumpFloodStep ( stepSize , output ) ; stepSize /= 2 ; } switch ( quality ) { case "JFA" : { break ; } case "JFA+1" : { this . _runJumpFloodStep ( 1 , JumpFloodOutput . FOR_SCREEN ) ; break ; } case "JFA+2" : { this . _runJumpFloodStep ( 4 , JumpFloodOutput . FOR_ANOTHER_STEP ) ; this . _runJumpFloodStep ( 3 , JumpFloodOutput . FOR_ANOTHER_STEP ) ; this . _runJumpFloodStep ( 2 , JumpFloodOutput . FOR_ANOTHER_STEP ) ; this . _runJumpFloodStep ( 1 , JumpFloodOutput . FOR_SCREEN ) ; break ; } } } public getPixels ( ) { const gl = this . _gl . gl ; const pixels = new Uint8Array ( gl . drawingBufferWidth * gl . drawingBufferHeight * 4 ) ; gl . readPixels ( 0 , 0 , gl . drawingBufferWidth , gl . drawingBufferHeight , gl . RGBA , gl . UNSIGNED_BYTE , pixels ) ; return pixels ; } private _setSeedsFromCanvas ( inputCanvas ) { if ( ! this . _seedInputTexture ) { throw new Error ( `Expected _seedInputTexture to be set before calling setSeedsFromCanvas` ) ; } const width = inputCanvas . width ; const height = inputCanvas . height ; this . _seedInputTexture . resize ( width , height , inputCanvas ) ; const material = this . _prepJumpFloodData ; material . setUniformSampler ( shaders . GLSLX_NAME_U_SEED_INPUT_TEXTURE , this . _seedInputTexture , 0 ) ; material . setUniformVec2 ( shaders . GLSLX_NAME_U_RESOLUTION , width , height ) ; this . _gl . setRenderTarget ( this . _sourceTextureTarget ) ; this . _gl . setViewport ( 0 , 0 , width , height ) ; this . _gl . draw ( Graphics . Primitive . TRIANGLE_STRIP , material , this . _quadBuffer ) ; } private _runJumpFloodStep ( stepSize , output ) { if ( ! this . _sourceTexture || ! this . _seedInputTexture ) { throw new Error ( `Expected textures to be set before calling setSeedsFromCanvas` ) ; } const { width , height } = this . _outputCanvas ; const material = output . format === "seed-position" ? this . _jumpFloodOutputSeedPosition : this . _jumpFloodOutputDistance ; material . setUniformSampler ( shaders . GLSLX_NAME_U_INPUT_TEXTURE , this . _sourceTexture , 0 ) ; material . setUniformInt ( shaders . GLSLX_NAME_U_STEP_SIZE , stepSize ) ; material . setUniformVec2 ( shaders . GLSLX_NAME_U_RESOLUTION , width , height ) ; if ( output . format === "distance" ) { material . setUniformSampler ( shaders . GLSLX_NAME_U_SEED_INPUT_TEXTURE , this . _seedInputTexture , 1 ) ; } this . _gl . setRenderTarget ( output . renderTarget === "texture" ? this . _destTextureTarget : null ) ; this . _gl . setViewport ( 0 , 0 , width , height ) ; this . _gl . draw ( Graphics . Primitive . TRIANGLE_STRIP , material , this . _quadBuffer ) ; this . _swapBuffers ( ) ; } private _swapBuffers ( ) { const tmp = this . _sourceTexture ; const tmpTarget = this . _sourceTextureTarget ; this . _sourceTexture = this . _destTexture ; this . _sourceTextureTarget = this . _destTextureTarget ; this . _destTexture = tmp ; this . _destTextureTarget = tmpTarget ; } } function nextPowerOfTwo ( n ) { n -- ; n |= n >> 1 ; n |= n >> 2 ; n |= n >> 4 ; n |= n >> 8 ; n |= n >> 16 ; n ++ ; return n ; } type Color = { r : number ; g : number ; b : number ; a : number ; } ; type JumpFloodOutput = { format : "distance" | "seed-position" ; renderTarget : "texture" | "screen" ; } ; namespace JumpFloodOutput { export const FOR_ANOTHER_STEP = { format : "seed-position" , renderTarget : "texture" , } ; export const FOR_SCREEN = { format : "distance" , renderTarget : "screen" , } ; }