import { objIsColor } from "./Color" ; import { Slider , Widget , RENDER_HACKS_ICON , createDOMFromString , HIGHLIGHT_COLOR , setElementHighlighted , Checkbox } from "./ui" ; import { GlobalGrabManager } from "./GrabManager" ; import { assert } from "./util" ; import { invlerp , lerp } from "./MathHelpers" ; import { IS_DEVELOPMENT } from "./BuildVersion" ; import "reflect-metadata" ; function getParentMetadata ( target , key ) { return { range : Reflect . getMetadata ( 'df:range' , target , key ) , usepercent : Reflect . getMetadata ( 'df:usepercent' , target , key ) , } ; } export class FloatingPanel implements Widget { public elem : HTMLElement ; public onclose : ( ( ) => void ) | null = null ; public customHeaderBackgroundColor : string = '' ; protected header : HTMLElement ; protected headerContainer : HTMLElement ; protected svgIcon : SVGSVGElement ; protected minimizeButton : HTMLElement ; protected minimized = false ; protected closeButton : HTMLElement ; private toplevel : HTMLElement ; public mainPanel : HTMLElement ; public contents : HTMLElement ; constructor ( ) { this . toplevel = document . createElement ( 'div' ) ; this . toplevel . style . color = 'white' ; this . toplevel . style . font = '16px monospace' ; this . toplevel . style . overflow = 'hidden' ; this . toplevel . style . display = 'grid' ; this . toplevel . style . gridAutoFlow = 'column' ; this . toplevel . style . gridGap = '20px' ; this . toplevel . style . alignItems = 'start' ; this . toplevel . style . outline = 'none' ; this . toplevel . style . minWidth = '300px' ; this . toplevel . style . position = 'absolute' ; this . toplevel . style . left = '82px' ; this . toplevel . style . top = '32px' ; this . toplevel . style . pointerEvents = 'auto' ; this . toplevel . tabIndex = - 1 ; this . mainPanel = document . createElement ( 'div' ) ; this . mainPanel . style . overflow = 'hidden' ; this . mainPanel . style . transition = '.25s ease-out' ; this . mainPanel . style . backgroundColor = 'rgba(0, 0, 0, 0.8)' ; this . toplevel . appendChild ( this . mainPanel ) ; this . headerContainer = document . createElement ( 'div' ) ; this . mainPanel . appendChild ( this . headerContainer ) ; this . header = document . createElement ( 'h1' ) ; this . header . style . lineHeight = '28px' ; this . header . style . margin = '0' ; this . header . style . fontSize = '100%' ; this . header . style . cursor = 'pointer' ; this . header . style . userSelect = 'none' ; this . header . style . display = 'grid' ; this . header . style . gridTemplateColumns = '28px 1fr 28px' ; this . header . style . alignItems = 'center' ; this . header . style . justifyItems = 'center' ; this . header . style . gridAutoFlow = 'column' ; this . header . onmousedown = ( e ) => { GlobalGrabManager . takeGrab ( this , e , { takePointerLock : false , useGrabbingCursor : true , releaseOnMouseUp : true } ) ; } ; this . header . ondblclick = ( e ) => { this . close ( ) ; } ; this . minimizeButton = document . createElement ( 'div' ) ; this . minimizeButton . textContent = '_' ; this . minimizeButton . title = 'Minimize' ; this . minimizeButton . style . gridColumn = '3' ; this . minimizeButton . style . lineHeight = '28px' ; this . minimizeButton . style . margin = '0' ; this . minimizeButton . style . padding = '0 4px' ; this . minimizeButton . style . fontSize = '100%' ; this . minimizeButton . style . cursor = 'pointer' ; this . minimizeButton . style . userSelect = 'none' ; this . minimizeButton . onclick = ( e ) => { this . setMinimized ( ! this . minimized ) ; } ; this . minimizeButton . ondblclick = ( e ) => { e . stopPropagation ( ) ; } ; this . closeButton = document . createElement ( 'div' ) ; this . closeButton . textContent = 'X' ; this . closeButton . title = 'Close' ; this . closeButton . style . gridColumn = '4' ; this . closeButton . style . lineHeight = '28px' ; this . closeButton . style . margin = '0' ; this . closeButton . style . padding = '0 4px' ; this . closeButton . style . fontSize = '100%' ; this . closeButton . style . cursor = 'pointer' ; this . closeButton . style . userSelect = 'none' ; this . closeButton . onclick = ( ) => { this . close ( ) ; } ; this . headerContainer . appendChild ( this . header ) ; this . contents = document . createElement ( 'div' ) ; this . contents . style . maxHeight = '50vh' ; this . contents . style . overflow = 'auto' ; this . mainPanel . appendChild ( this . contents ) ; this . setWidth ( 400 ) ; this . elem = this . toplevel ; } public setWidth ( v ) { this . header . style . width = `${ v }px` ; this . contents . style . width = `${ v }px` ; } public close ( ) { if ( this . toplevel . parentElement !== null ) this . toplevel . parentElement . removeChild ( this . toplevel ) ; if ( this . onclose ) this . onclose ( ) ; } public onMotion ( dx , dy ) { this . toplevel . style . left = ( parseFloat ( this . toplevel . style . left ! ) + dx ) + 'px' ; this . toplevel . style . top = ( parseFloat ( this . toplevel . style . top ! ) + dy ) + 'px' ; } public onGrabReleased ( ) { } public setVisible ( v ) { this . toplevel . style . display = v ? 'grid' : 'none' ; } public setMinimized ( v ) { if ( this . minimized === v ) return ; this . minimized = v ; this . contents . style . height = this . minimized ? '0px' : '' ; } public setTitle ( icon , title ) { this . svgIcon = createDOMFromString ( icon ) . querySelector ( 'svg' ) ! ; this . svgIcon . style . gridColumn = '1' ; this . header . textContent = title ; this . header . appendChild ( this . svgIcon ) ; this . header . appendChild ( this . minimizeButton ) ; this . header . appendChild ( this . closeButton ) ; this . toplevel . dataset . title = title ; this . syncHeaderStyle ( ) ; } protected syncHeaderStyle ( ) { if ( this . customHeaderBackgroundColor ) { this . svgIcon . style . fill = '' ; this . header . style . backgroundColor = this . customHeaderBackgroundColor ; this . header . style . color = 'white' ; } else { this . svgIcon . style . fill = 'black' ; setElementHighlighted ( this . header , true , HIGHLIGHT_COLOR ) ; } } public bindCheckbox ( labelName , obj , paramName ) { let value = obj [ paramName ] ; assert ( typeof value === "boolean" ) ; const cb = new Checkbox ( labelName , value ) ; cb . onchanged = ( ) => { obj [ paramName ] = cb . checked ; update ( ) ; } ; function update ( ) { cb . setChecked ( obj [ paramName ] ) ; } setInterval ( ( ) => { if ( obj [ paramName ] !== value ) update ( ) ; } , 100 ) ; this . contents . appendChild ( cb . elem ) ; } public bindSingleSlider ( labelName , obj , paramName , parentMetadata = null , midiControls = null ) { let value = obj [ paramName ] ; assert ( typeof value === "number" ) ; let min = 0 , max = 1 , step = 0.01 ; let range = Reflect . getMetadata ( 'df:range' , obj , paramName ) ; if ( range === undefined && parentMetadata !== null ) range = parentMetadata . range ; if ( range !== undefined ) { min = range . min ; max = range . max ; step = range . step ; } let usePercent = Reflect . getMetadata ( 'df:usepercent' , obj , paramName ) ; if ( usePercent === undefined && parentMetadata !== null ) usePercent = parentMetadata . usepercent ; usePercent = ! ! usePercent ; let labelNameMetadata = Reflect . getMetadata ( 'df:label' , obj , paramName ) ; if ( labelNameMetadata === undefined && parentMetadata !== null && parentMetadata . label !== undefined ) labelNameMetadata = parentMetadata . label ; if ( labelNameMetadata !== undefined ) labelName = labelNameMetadata ; const fracDig = Math . max ( 0 , - Math . log10 ( step ) ) ; const slider = new Slider ( ) ; let midiBindButton = null ; if ( midiControls !== null && midiControls . isInitialized ( ) ) { const sliderDiv = slider . elem . querySelector ( 'div' ) ! ; sliderDiv . style . gridTemplateColumns = '48px 1fr 1fr' ; let bindState = 'unbound' ; const syncColor = ( ) : void => { if ( bindState === 'unbound' ) midiBindButton ! . style . color = 'white' ; else if ( bindState === 'binding' ) midiBindButton ! . style . color = 'cyan' ; else if ( bindState === 'bound' ) midiBindButton ! . style . color = HIGHLIGHT_COLOR ; } ; const midiListener = { onbind : ( v ) => { bindState = 'bound' ; syncColor ( ) ; } , onunbind : ( ) => { bindState = 'unbound' ; syncColor ( ) ; } , onvalue : ( v ) => { const newValue = lerp ( min , max , v ) ; onvalue ( newValue ) ; } , } ; midiBindButton = document . createElement ( 'span' ) ; midiBindButton . textContent = 'B' ; midiBindButton . style . color = 'white' ; midiBindButton . style . margin = '0 1em' ; midiBindButton . style . userSelect = 'none' ; midiBindButton . style . cursor = 'pointer' ; midiBindButton . style . fontWeight = 'bold' ; midiBindButton . onclick = ( ) => { if ( bindState === 'unbound' ) { bindState = 'binding' ; syncColor ( ) ; midiControls . setNextBindListener ( midiListener ) ; } else if ( bindState === 'binding' ) { bindState = 'unbound' ; syncColor ( ) ; midiControls . setNextBindListener ( null ) ; } else if ( bindState === 'bound' ) { bindState = 'unbound' ; syncColor ( ) ; midiListener . unbind ! ( ) ; } } ; sliderDiv . insertBefore ( midiBindButton , sliderDiv . firstElementChild ) ; } const onvalue = ( newValue ) : void => { obj [ paramName ] = newValue ; update ( ) ; } ; slider . onvalue = onvalue ; update ( ) ; function update ( ) { value = obj [ paramName ] ; let valueStr ; if ( usePercent ) { valueStr = `${ ( invlerp ( min , max , value ) * 100 ) . toFixed ( 0 ) }%` ; } else { valueStr = value . toFixed ( fracDig ) ; } slider . setLabel ( `${ labelName } = ${ valueStr }` ) ; const localMin = Math . min ( value , min ) ; const localMax = Math . max ( value , max ) ; if ( range === undefined ) { min = localMin ; max = localMax ; } slider . setRange ( localMin , localMax , step ) ; slider . setValue ( value ) ; } setInterval ( ( ) => { if ( obj [ paramName ] !== value ) update ( ) ; } , 100 ) ; this . contents . appendChild ( slider . elem ) ; } public bindSliderChain ( labelName , target , ... args ) { let parentMetadata = null ; for ( let i = 0 ; i < args . length - 1 ; i ++ ) { parentMetadata = getParentMetadata ( target , args [ i ] ) ; target = target [ args [ i ] ] ; } this . bindSingleSlider ( labelName , target , args [ args . length - 1 ] , parentMetadata ) ; } } export class DebugFloaterHolder { private floatingPanels : FloatingPanel [ ] = [ ] ; private debugFloater : FloatingPanel | null = null ; public elem : HTMLElement ; public midiControls = new GlobalMIDIControls ( ) ; constructor ( ) { this . elem = document . createElement ( 'div' ) ; if ( IS_DEVELOPMENT ) this . midiControls . init ( ) ; } public makeFloatingPanel ( title = 'Floating Panel' , icon = RENDER_HACKS_ICON ) { const panel = new FloatingPanel ( ) ; panel . setWidth ( 600 ) ; panel . setTitle ( icon , title ) ; panel . onclose = ( ) => { if ( this . debugFloater === panel ) this . debugFloater = null ; } ; this . elem . appendChild ( panel . elem ) ; this . floatingPanels . push ( panel ) ; return panel ; } private getDebugFloater ( ) { if ( this . debugFloater === null ) this . debugFloater = this . makeFloatingPanel ( 'Debug' ) ; return this . debugFloater ; } public destroyScene ( ) { for ( let i = 0 ; i < this . floatingPanels . length ; i ++ ) this . floatingPanels [ i ] . close ( ) ; this . floatingPanels = [ ] ; this . debugFloater = null ; } private _bindPanelRecurse ( obj , panel , parentName , parentMetadata = null ) { const childDefaultVisible = objIsColor ( obj ) || ( obj instanceof Array ) || ( obj instanceof Float32Array ) ; const keys = Object . keys ( obj ) ; for ( let i = 0 ; i < keys . length ; i ++ ) { const keyName = keys [ i ] ; if ( ! ( childDefaultVisible || dfShouldShowOwn ( obj , keyName ) ) ) continue ; const v = obj [ keyName ] ; if ( typeof v === "number" ) panel . bindSingleSlider ( `${ parentName }.${ keyName }` , obj , keyName , parentMetadata , this . midiControls ) ; else if ( typeof v === "boolean" ) panel . bindCheckbox ( `${ parentName }.${ keyName }` , obj , keyName ) ; ; this . _bindPanelRecurse ( v , panel , `${ parentName }.${ keyName }` , getParentMetadata ( obj , keyName ) ) ; } } public bindPanel ( obj , panel = null ) { if ( panel === null ) panel = this . getDebugFloater ( ) ; while ( panel . contents . firstChild ) panel . contents . removeChild ( panel . contents . firstChild ) ; this . _bindPanelRecurse ( obj , panel , '' ) ; } } function dfShouldShowOwn ( obj , keyName ) { const visibility = Reflect . getMetadata ( 'df:visibility' , obj , keyName ) ; if ( visibility === false ) return false ; if ( visibility === true ) return true ; if ( Reflect . hasMetadata ( 'df:range' , obj , keyName ) ) return true ; if ( Reflect . hasMetadata ( 'df:sigfigs' , obj , keyName ) ) return true ; return false ; } interface MIDIControlListener { onvalue : ( ( v : number ) => void ) | null ; onbind : ( ( v : number ) => void ) | null ; onunbind : ( ( ) => void ) | null ; unbind ? ( ) : void ; } class BoundMIDIControl { public value : number = - 1 ; public listener : MIDIControlListener | null = null ; constructor ( public channel , public controlNumber ) { } public bindListener ( v ) { if ( this . listener !== null ) this . listener . unbind = undefined ; if ( this . listener !== null && this . listener . onunbind !== null ) this . listener . onunbind ( ) ; this . listener = v ; if ( this . listener !== null ) this . listener . unbind = ( ) => this . bindListener ( null ) ; if ( this . listener !== null && this . listener . onbind !== null ) this . listener . onbind ( this . value ) ; } public setValue ( v ) { if ( this . listener !== null && this . listener . onvalue !== null ) this . listener . onvalue ( v ) ; } } class GlobalMIDIControls { private midiAccess : WebMidi . MIDIAccess | null = null ; private boundControls : BoundMIDIControl [ ] = [ ] ; public async init ( ) { if ( navigator . requestMIDIAccess === undefined ) return ; this . midiAccess = await navigator . requestMIDIAccess ( { sysex : false } ) ; this . midiAccess . onstatechange = ( ) => { this . bindInputs ( ) ; } ; this . bindInputs ( ) ; } public isInitialized ( ) { return this . midiAccess !== null ; } private bindInputs ( ) { for ( const input of this . midiAccess ! . inputs . values ( ) ) input . onmidimessage = this . onMessage ; } private nextListener : MIDIControlListener | null = null ; public setNextBindListener ( v ) { if ( this . nextListener !== null && this . nextListener . onunbind !== null ) { this . nextListener . onunbind ( ) ; this . nextListener = null ; } this . nextListener = v ; } private onControlMessage ( channel , controlNumber , value ) { let boundControl = this . boundControls . find ( ( control ) => control . channel === channel && control . controlNumber === controlNumber ) ; const normalizedValue = invlerp ( 0 , 127 , value ) ; if ( this . nextListener !== null ) { if ( boundControl === undefined ) { boundControl = new BoundMIDIControl ( channel , controlNumber ) ; this . boundControls . push ( boundControl ) ; } boundControl . bindListener ( this . nextListener ) ; this . nextListener = null ; boundControl . setValue ( normalizedValue ) ; } else if ( boundControl !== undefined ) { boundControl . setValue ( normalizedValue ) ; } } private onMessage = ( e ) : void => { const messageType = e . data [ 0 ] ; if ( messageType >= 0xB0 && messageType <= 0xBF ) { this . onControlMessage ( e . data [ 0 ] , e . data [ 1 ] , e . data [ 2 ] ) ; } } ; } export function dfShow ( ) { return Reflect . metadata ( 'df:visibility' , true ) ; } export function dfHide ( ) { return Reflect . metadata ( 'df:visibility' , false ) ; } export function dfRange ( min = 0 , max = 1 , step = ( max - min ) / 100 ) { return Reflect . metadata ( 'df:range' , { min , max , step } ) ; } export function dfSigFigs ( v = 2 ) { return Reflect . metadata ( 'df:sigfigs' , v ) ; } export function dfUsePercent ( v = true ) { return Reflect . metadata ( 'df:usepercent' , v ) ; } export function dfLabel ( v ) { return Reflect . metadata ( 'df:label' , v ) ; }