import { WindowManager } from '../wnd/window_manager' ; import { Wnd } from '../wnd/wnd' ; import { MenuItemInfo , WndEvent } from '../wnd/types' ; import { DomUtil } from '../util/dom_util' ; import { Nes } from '../nes/nes' ; import { MirrorMode } from '../nes/ppu/types' ; import { Scaler , NearestNeighborScaler , ScanlineScaler , EpxScaler } from '../util/scaler' ; import { App } from './app' ; import { AppEvent } from './app_event' ; import { GlobalSetting } from './global_setting' ; import { PadBit , PadValue } from '../nes/apu' ; import { PadKeyHandler } from '../util/pad_key_handler' ; import { GamepadManager } from '../util/gamepad_manager' ; import { RegisterWnd , RamWnd , TraceWnd , ControlWnd } from './debug_wnd' ; import { FpsWnd , PaletWnd , NameTableWnd , PatternTableWnd , AudioWnd } from './other_wnd' ; import { Fds } from '../nes/fds/fds' ; import { FdsCtrlWnd } from './fds_ctrl_wnd' ; import * as Pubsub from '../util/pubsub' ; const WIDTH = 256 | 0 ; const HEIGHT = 240 | 0 ; const HEDGE = 4 | 0 ; const VEDGE = 8 | 0 ; const TRANSITION_DURATION = '0.1s' ; const TIME_SCALE_NORMAL = 1 ; const TIME_SCALE_FAST = 4 ; const enum WndType { PALET = 1 , NAME , PATTERN , AUDIO , REGISTER , RAM , TRACE , CONTROL , FPS , FDS_CTRL } const enum ScalerType { NEAREST , SCANLINE , EPX } function takeScreenshot ( wndMgr , screenWnd ) { const img = document . createElement ( 'img' ) ; const title = String ( Date . now ( ) ) ; img . src = screenWnd . capture ( ) ; img . className = 'pixelated full-size' ; img . title = img . alt = title ; const imgWnd = new Wnd ( wndMgr , WIDTH , HEIGHT , title ) ; imgWnd . setContent ( img ) ; imgWnd . addResizeBox ( ) ; wndMgr . add ( imgWnd ) ; return imgWnd ; } function fitAspectRatio ( width , height , ratio ) { if ( width / height >= ratio ) width = height * ratio ; else height = width / ratio ; return [ width , height ] ; } function maxSize ( wndMgr , overscan ) { const rootRect = wndMgr . getRootClientRect ( ) ; const maxWidth = rootRect . width - 2 ; const maxHeight = rootRect . height - Wnd . TITLEBAR_HEIGHT - Wnd . MENUBAR_HEIGHT - 2 ; const w = Math . round ( WIDTH - ( overscan ? HEDGE * 2 : 0 ) ) ; const h = Math . round ( HEIGHT - ( overscan ? VEDGE * 2 : 0 ) ) ; const [ width , height ] = fitAspectRatio ( maxWidth , maxHeight , w / h ) ; return { rootRect , width , height } ; } export class ScreenWnd extends Wnd { protected subscription : Pubsub . Subscription ; private fullscreenBase : HTMLElement ; private canvasHolder : HTMLElement ; private scaler : Scaler ; private overscan = true ; private contentWidth = 0 ; private contentHeight = 0 ; private menuItems : Array < MenuItemInfo > ; private scalerType = ScalerType . NEAREST ; private padKeyHandler = new PadKeyHandler ( ) ; private timeScale = 1 ; private fullscreenResizeFunc : ( ) => void ; private repeatBtnFrame = false ; protected wndMap = new Array < Wnd | null > ( ) ; constructor ( wndMgr , protected app , protected nes , protected stream ) { super ( wndMgr , ( WIDTH - HEDGE * 2 ) * 2 , ( HEIGHT - VEDGE * 2 ) * 2 + Wnd . MENUBAR_HEIGHT , 'NES' ) ; if ( app == null || nes == null || stream == null ) return ; this . setUpMenuBar ( ) ; this . contentHolder . style . overflow = 'hidden' ; this . fullscreenBase = document . createElement ( 'div' ) ; this . fullscreenBase . className = 'full-size' ; DomUtil . setStyles ( this . fullscreenBase , { position : 'relative' , overflow : 'hidden' , } ) ; this . setContent ( this . fullscreenBase ) ; this . canvasHolder = document . createElement ( 'div' ) ; this . canvasHolder . style . transitionDuration = TRANSITION_DURATION ; this . fullscreenBase . appendChild ( this . canvasHolder ) ; this . setScaler ( ScalerType . NEAREST ) ; this . addResizeBox ( ) ; this . subscription = this . stream . subscribe ( ( type , param ? ) => { switch ( type ) { case AppEvent . Type . RENDER : this . render ( ) ; break ; case AppEvent . Type . RESET : this . scaler . reset ( ) ; break ; case AppEvent . Type . CLOSE_WND : { const wnd = param as Wnd ; const i = this . wndMap . indexOf ( wnd ) ; if ( i >= 0 ) this . wndMap [ i ] = null ; } break ; } } ) ; this . contentWidth = ( WIDTH - HEDGE * 2 ) * 2 ; this . contentHeight = ( HEIGHT - VEDGE * 2 ) * 2 ; this . updateContentSize ( this . contentWidth , this . contentHeight ) ; { const { width , height } = maxSize ( this . wndMgr , this . overscan ) ; if ( width < ( WIDTH - HEDGE * 2 ) * 2 ) { this . setClientSize ( width , height ) ; } } this . fullscreenResizeFunc = ( ) => { const bounding = document . body . getBoundingClientRect ( ) ; let width = bounding . width ; let height = bounding . height ; if ( width / height >= WIDTH / HEIGHT ) { width = ( height * ( WIDTH / HEIGHT ) ) | 0 ; } else { height = ( width * ( HEIGHT / WIDTH ) ) | 0 ; } DomUtil . setStyles ( this . fullscreenBase , { width : `${ width }px` , height : `${ height }px` , margin : 'auto' , } ) ; this . updateContentSize ( width , height ) ; } ; wndMgr . add ( this ) ; if ( window . $DEBUG ) { this . createPaletWnd ( ) ; this . createNameTableWnd ( ) ; this . createPatternTableWnd ( ) ; this . createTraceWnd ( ) ; this . createRegisterWnd ( ) ; this . createControlWnd ( ) ; } } public getTimeScale ( ) { return this . timeScale ; } public onEvent ( event , param ? ) { switch ( event ) { case WndEvent . DRAG_BEGIN : if ( GlobalSetting . pauseOnMenu ) this . stream . triggerPauseApp ( ) ; break ; case WndEvent . DRAG_END : if ( GlobalSetting . pauseOnMenu ) this . stream . triggerResumeApp ( ) ; break ; case WndEvent . RESIZE_BEGIN : this . canvasHolder . style . transitionDuration = '0s' ; if ( GlobalSetting . pauseOnMenu ) this . stream . triggerPauseApp ( ) ; break ; case WndEvent . RESIZE_END : this . canvasHolder . style . transitionDuration = TRANSITION_DURATION ; if ( GlobalSetting . pauseOnMenu ) this . stream . triggerResumeApp ( ) ; break ; case WndEvent . RESIZE_MOVE : { const { width , height } = param ; this . onResized ( width , height ) ; } break ; case WndEvent . OPEN_MENU : if ( GlobalSetting . pauseOnMenu ) this . stream . triggerPauseApp ( ) ; break ; case WndEvent . CLOSE_MENU : if ( GlobalSetting . pauseOnMenu ) this . stream . triggerResumeApp ( ) ; break ; case WndEvent . UPDATE_FRAME : { const elapsed = param as number ; this . update ( elapsed ) ; } break ; case WndEvent . FOCUS : if ( ! param ) { this . timeScale = TIME_SCALE_NORMAL ; this . padKeyHandler . clearAll ( ) ; } break ; default : break ; } } public onResized ( width , height ) { this . contentWidth = width ; this . contentHeight = height ; this . updateContentSize ( width , height - Wnd . MENUBAR_HEIGHT ) ; } public setClientSize ( width , height ) { width = Math . round ( width ) ; height = Math . round ( height ) ; super . setClientSize ( width , height ) ; this . contentWidth = width ; this . contentHeight = height ; this . updateContentSize ( width , height ) ; } public capture ( ) { return this . scaler . getCanvas ( ) . toDataURL ( ) ; } public getPadStatus ( padNo ) { if ( ! this . isTop ( ) || this . wndMgr . isBlur ( ) ) return 0 ; let state = this . padKeyHandler . getStatus ( padNo ) | GamepadManager . getState ( padNo ) ; if ( this . repeatBtnFrame ) state |= ( state & ( PadValue . REPEAT_A | PadValue . REPEAT_B ) ) >> ( PadBit . REPEAT_A - PadBit . A ) ; return state ; } public setFullscreen ( ) { window . addEventListener ( 'resize' , this . fullscreenResizeFunc ) ; return this . wndMgr . setFullscreen ( this . contentHolder , isFullscreen => { if ( ! isFullscreen ) { window . removeEventListener ( 'resize' , this . fullscreenResizeFunc ) ; DomUtil . setStyles ( this . fullscreenBase , { width : '' , height : '' , margin : '' , } ) ; DomUtil . setStyles ( this . contentHolder , { backgroundColor : '' , display : '' , } ) ; this . updateContentSize ( this . contentWidth , this . contentHeight ) ; } else { DomUtil . setStyles ( this . contentHolder , { backgroundColor : 'black' , display : 'flex' , } ) ; } this . contentHolder . focus ( ) ; } ) ; } public close ( ) { this . closeChildrenWindows ( ) ; if ( this . subscription != null ) this . subscription . unsubscribe ( ) ; this . stream . triggerCloseWnd ( this ) ; super . close ( ) ; } public render ( ) { this . scaler . render ( this . nes ) ; } public createFdsCtrlWnd ( fds ) { return this . createSubWnd ( WndType . FDS_CTRL , ( ) => { return new FdsCtrlWnd ( this . wndMgr , fds ) ; } ) ; } protected closeChildrenWindows ( ) { for ( const wnd of Object . values ( this . wndMap ) ) if ( wnd != null ) wnd . close ( ) ; } protected setClientScale ( scale ) { const w = ( ( WIDTH - ( this . overscan ? HEDGE * 2 : 0 ) ) * scale ) | 0 ; const h = ( ( HEIGHT - ( this . overscan ? VEDGE * 2 : 0 ) ) * scale ) | 0 ; this . setClientSize ( w , h ) ; } protected updateContentSize ( width , height ) { if ( ! this . fullscreenBase ) return ; const w = ! this . overscan ? width : ( width * ( WIDTH / ( WIDTH - HEDGE * 2 ) ) ) | 0 ; const h = ! this . overscan ? height : ( height * ( HEIGHT / ( HEIGHT - VEDGE * 2 ) ) ) | 0 ; const left = ! this . overscan ? 0 : - ( w * HEDGE / WIDTH ) | 0 ; const top = ! this . overscan ? 0 : - ( h * VEDGE / HEIGHT ) | 0 ; DomUtil . setStyles ( this . canvasHolder , { position : 'absolute' , width : `${ w }px` , height : `${ h }px` , top : `${ top }px` , left : `${ left }px` , } ) ; } protected setUpMenuBar ( ) { this . menuItems = [ { label : 'File' , submenu : [ { label : 'Pause' , checked : ( ) => this . nes . getCpu ( ) . isPaused ( ) , click : ( ) => { if ( this . nes . getCpu ( ) . isPaused ( ) ) this . stream . triggerRun ( ) ; else this . stream . triggerPause ( ) ; } , } , { label : 'Reset' , click : ( ) => { this . stream . triggerReset ( ) ; this . stream . triggerRun ( ) ; } , } , { label : 'Screenshot' , click : ( ) => { takeScreenshot ( this . wndMgr , this ) ; } , } , { label : '----' } , { label : 'Save status' , click : ( ) => this . app . saveData ( ) , } , { label : 'Download status to file' , click : ( ) => this . app . saveDataAs ( ) , } , { label : '----' } , { label : 'Load status' , disabled : ( ) => ! this . app . hasSaveData ( ) , click : ( ) => this . app . loadData ( ) , } , { label : 'Restore status from file' , click : ( ) => this . app . loadDataFromFile ( ) , } , { label : '----' } , { label : 'Quit' , click : ( ) => { this . close ( ) ; } , } , ] , } , { label : 'View' , submenu : [ { label : '1x1' , checked : ( ) => this . isAspectRatio ( 1 ) , click : ( ) => { this . setClientScale ( 1 ) ; } , } , { label : '2x2' , checked : ( ) => this . isAspectRatio ( 2 ) , click : ( ) => { this . setClientScale ( 2 ) ; } , } , { label : 'Adjust aspect ratio' , disabled : ( ) => this . isAspectRatio ( 0 ) , click : ( ) => { this . adjustAspectRatio ( ) ; } , } , { label : '----' } , { label : 'Fullscreen' , click : ( ) => { this . setFullscreen ( ) ; } , } , { label : '----' } , { label : 'Overscan' , checked : ( ) => this . overscan , click : ( ) => { this . toggleOverscan ( ) ; } , } , { label : 'Sprite flicker' , checked : ( ) => ! this . nes . getPpu ( ) . suppressSpriteFlicker , click : ( ) => { this . toggleSpriteFlicker ( ) ; } , } , ] , } , { label : 'Scaler' , submenu : [ { label : 'Nearest' , checked : ( ) => this . scalerType === ScalerType . NEAREST , click : ( ) => { this . setScaler ( ScalerType . NEAREST ) ; } , } , { label : 'Scanline' , checked : ( ) => this . scalerType === ScalerType . SCANLINE , click : ( ) => { this . setScaler ( ScalerType . SCANLINE ) ; } , } , { label : 'Epx' , checked : ( ) => this . scalerType === ScalerType . EPX , click : ( ) => { this . setScaler ( ScalerType . EPX ) ; } , } , ] , } , { label : 'Audio' , submenu : [ { label : 'Quantized Triangle' , checked : ( ) => this . app . getAudioBlockiness ( ) , click : ( ) => { this . app . toggleAudioBlockiness ( ) ; } , } , { label : '----' } , { label : 'Keyboard' , click : ( ) => { this . createAudioWnd ( ) ; } , } , ] , } , { label : 'Debug' , submenu : [ { label : 'Palette' , click : ( ) => { this . createPaletWnd ( ) ; } , } , { label : 'NameTable' , click : ( ) => { this . createNameTableWnd ( ) ; } , } , { label : 'PatternTable' , click : ( ) => { this . createPatternTableWnd ( ) ; } , } , { label : 'FPS' , click : ( ) => { this . createFpsWnd ( ) ; } , } , { label : '----' } , { label : 'Trace' , click : ( ) => { this . createTraceWnd ( ) ; } , } , { label : 'Registers' , click : ( ) => { this . createRegisterWnd ( ) ; } , } , { label : 'Ram' , click : ( ) => { this . createRamWnd ( ) ; } , } , { label : 'Control' , click : ( ) => { this . createControlWnd ( ) ; } , } , ] , } , ] ; this . addMenuBar ( this . menuItems ) ; } protected maximize ( ) { const { rootRect , width , height } = maxSize ( this . wndMgr , this . overscan ) ; const x = ( rootRect . width - ( width + 2 ) ) / 2 ; const y = ( rootRect . height - ( height + Wnd . TITLEBAR_HEIGHT + Wnd . MENUBAR_HEIGHT + 2 ) ) / 2 ; this . setClientSize ( width , height ) ; this . setPos ( Math . round ( x ) , Math . round ( y ) ) ; } protected createSubWnd ( wndType , generate ) { if ( this . wndMap [ wndType ] != null ) { this . wndMgr . moveToTop ( this . wndMap [ wndType ] ! ) ; return false ; } const wnd = generate ( ) ; this . wndMap [ wndType ] = wnd ; return true ; } protected createPaletWnd ( ) { return this . createSubWnd ( WndType . PALET , ( ) => { const wnd = new PaletWnd ( this . wndMgr , this . nes , this . stream ) ; wnd . setPos ( 520 , 0 ) ; return wnd ; } ) ; } protected createNameTableWnd ( ) { return this . createSubWnd ( WndType . NAME , ( ) => { const ppu = this . nes . getPpu ( ) ; const wnd = new NameTableWnd ( this . wndMgr , ppu , this . stream , ppu . getMirrorMode ( ) === MirrorMode . HORZ ) ; wnd . setPos ( 520 , 40 ) ; return wnd ; } ) ; } protected createPatternTableWnd ( ) { return this . createSubWnd ( WndType . PATTERN , ( ) => { const getSelectedPalets = ( buf ) : boolean => { const paletWnd = this . wndMap [ WndType . PALET ] as PaletWnd ; if ( paletWnd == null ) return false ; paletWnd . getSelectedPalets ( buf ) ; return true ; } ; const wnd = new PatternTableWnd ( this . wndMgr , this . nes . getPpu ( ) , this . stream , getSelectedPalets ) ; wnd . setPos ( 520 , 300 ) ; return wnd ; } ) ; } protected createAudioWnd ( ) { return this . createSubWnd ( WndType . AUDIO , ( ) => new AudioWnd ( this . wndMgr , this . nes , this . stream ) ) ; } protected createTraceWnd ( ) { return this . createSubWnd ( WndType . TRACE , ( ) => { const wnd = new TraceWnd ( this . wndMgr , this . nes , this . stream ) ; wnd . setPos ( 0 , 500 ) ; return wnd ; } ) ; } protected createRegisterWnd ( ) { return this . createSubWnd ( WndType . REGISTER , ( ) => { const wnd = new RegisterWnd ( this . wndMgr , this . nes , this . stream ) ; wnd . setPos ( 410 , 500 ) ; return wnd ; } ) ; } protected createRamWnd ( ) { if ( this . wndMap [ WndType . RAM ] != null ) return false ; const wnd = new RamWnd ( this . wndMgr , this . nes , this . stream ) ; this . wndMgr . add ( wnd ) ; this . wndMap [ WndType . RAM ] = wnd ; return true ; } protected createControlWnd ( ) { return this . createSubWnd ( WndType . CONTROL , ( ) => { const wnd = new ControlWnd ( this . wndMgr , this . stream ) ; wnd . setPos ( 520 , 500 ) ; return wnd ; } ) ; } protected createFpsWnd ( ) { return this . createSubWnd ( WndType . FPS , ( ) => new FpsWnd ( this . wndMgr , this . stream ) ) ; } private update ( elapsedTime ) { this . padKeyHandler . update ( this . wndMgr . getKeyboardManager ( ) ) ; const speedUp = ( this . isTop ( ) && this . wndMgr . getKeyboardManager ( ) . getKeyPressing ( 'ShiftLeft' ) ) ; this . timeScale = speedUp ? TIME_SCALE_FAST : TIME_SCALE_NORMAL ; this . stream . triggerStartCalc ( ) ; this . stream . triggerUpdate ( elapsedTime ) ; this . stream . triggerEndCalc ( ) ; this . repeatBtnFrame = ! this . repeatBtnFrame ; } private isAspectRatio ( scale ) { const rect = this . contentHolder . getBoundingClientRect ( ) ; const w = WIDTH - ( this . overscan ? HEDGE * 2 : 0 ) ; const h = HEIGHT - ( this . overscan ? VEDGE * 2 : 0 ) ; if ( scale > 0 ) return Math . abs ( rect . width - w * scale ) < 0.5 && Math . abs ( rect . height - h * scale ) < 0.5 ; return Math . abs ( rect . width / rect . height - w / h ) < 0.005 ; } private adjustAspectRatio ( ) { const rect = this . contentHolder . getBoundingClientRect ( ) ; const w = WIDTH - ( this . overscan ? HEDGE * 2 : 0 ) ; const h = HEIGHT - ( this . overscan ? VEDGE * 2 : 0 ) ; const [ width , height ] = fitAspectRatio ( rect . width , rect . height , w / h ) ; this . setClientSize ( width , height ) ; } private toggleOverscan ( ) { this . overscan = ! this . overscan ; this . updateContentSize ( this . contentHolder . offsetWidth , this . contentHolder . offsetHeight ) ; } private toggleSpriteFlicker ( ) { const ppu = this . nes . getPpu ( ) ; ppu . suppressSpriteFlicker = ! ppu . suppressSpriteFlicker ; } private setScaler ( type ) { const initial = this . scaler == null ; if ( this . scalerType === type && ! initial ) return ; this . scalerType = type ; switch ( type ) { case ScalerType . NEAREST : this . scaler = new NearestNeighborScaler ( ) ; break ; case ScalerType . SCANLINE : this . scaler = new ScanlineScaler ( ) ; break ; case ScalerType . EPX : this . scaler = new EpxScaler ( ) ; break ; } DomUtil . removeAllChildren ( this . canvasHolder ) ; this . canvasHolder . appendChild ( this . scaler . getCanvas ( ) ) ; if ( ! initial ) this . render ( ) ; } }