Fixed issues with cameras in HTML5 target without webgl

Strict

Import mojo
Import reflection

Import flxextern
Import flxbasic
Import flxstate
Import flxcamera
Import flxtimer
Import flxtext
Import flxg
Import flxu
Import flxtilemap
Import system.flxassetsmanager
Import system.flxfont
Import system.flxdebugger
Import system.flxreplay
Import plugin.timermanager

#If TARGET = "xna" Or TARGET = "psm" Or TARGET = "win8"
	Import "data/beep_flx.wav"
#ElseIf TARGET = "html5"
	Import "data/beep_flx.ogg"
	Import "data/beep_flx.mp3"
#ElseIf TARGET = "glfw" Or TARGET = "bmax"
	Import "data/beep_flx.ogg"
#Else
	Import "data/beep_flx.mp3"
#End

Class FlxGame extends App

	Field useSoundHotKeys:Bool

	Field useSystemCursor:Bool

	Field forceDebugger:Bool

	Field _state:FlxState

	Field _requestedState:FlxState

	Field _requestedReset:Bool

	Field _debugger:FlxDebugger

	Field _debuggerUp:Bool

	Field _replay:FlxReplay

	Field _replayRequested:Bool

	Field _recordingRequested:Bool

	Field _replaying:Bool

	Field _recording:Bool

	Field _replayCancelKeys:Int[]

	Field _replayTimer:Float

	Field _replayCallback:FlxReplayListener

Private
	Field _iState:ClassInfo

	Field _step:Int

	Field _soundTrayTimer:Float

	Field _soundTrayWidth:Float

	Field _soundTrayHeight:Float

	Field _soundTrayX:Float

	Field _soundTrayY:Float

	Field _soundTrayVisible:Bool

	Field _soundTrayLabel:FlxText

	Field _updaterate:Int

	Field _switchStateListener:FlxSwitchStateListener

	'note: BUG: It seems we have bug in HTML5 vesion fo mojo
#If TARGET = "html5" And FLX_WEBGL_ENABLED = "0"

	Field _stateIsReady:Bool

#End

Public
	Method New(gameSizeX:Int, gameSizeY:Int, initialState:ClassInfo, zoom:Float = 1, updaterate:Int = 60, useSystemCursor:Bool = False)
		Local classObject:GlobalInfo

		For Local classInfo:ClassInfo = EachIn GetClasses()
			classObject = classInfo.GetGlobal("__CLASS__", False)

			If (classObject <> Null) Then
				classObject.SetValue(classInfo)
			End If
		Next

		_soundTrayTimer = 0
		_soundTrayWidth = 160
		_soundTrayHeight = 45
		_soundTrayX = 0
		_soundTrayY	= -_soundTrayHeight
		_soundTrayVisible = False

		FlxG.Init(Self, gameSizeX, gameSizeY, zoom)
		FlxG.Updaterate = updaterate

		_state = Null

		useSoundHotKeys = Not FlxG.Mobile
		Self.useSystemCursor = useSystemCursor

		If (Not useSystemCursor) HideMouse()

		forceDebugger = False
		_debuggerUp = False

		_replay = New FlxReplay()
		_replayRequested = False
		_recordingRequested = False
		_replaying = False
		_recording = False

		_iState = initialState
		_requestedState = Null
		_requestedReset = True
	End Method

	Method OnCreate:Int()
		_InitData()
		_Reset()

		#Rem
		_soundTrayLabel = New FlxText(10, 32, _soundTrayWidth, "VOLUME")
		_soundTrayLabel.SetFormat(FlxText.SYSTEM_FONT, 16, FlxG.WHITE, FlxText.ALIGN_CENTER)
		#End
		Return 0
	End Method

	Method OnUpdate:Int()
		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If

	#If FLX_ASYNC_EVENTS_ENABLED = "1"
		UpdateAsyncEvents()
	#End

		FlxG.UpdateDevice()
		_Step()

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm"
		If (useSoundHotKeys) Then
			If (KeyHit(KEY_0)) Then
				FlxG.Mute = Not FlxG.Mute

				If (FlxG.VolumeHandler <> Null) Then
					If (FlxG.Mute) Then
						FlxG.VolumeHandler.OnVolumeChange(0)
					Else
						FlxG.VolumeHandler.OnVolumeChange(FlxG.Volume())
					End If
				End If

				_ShowSoundTray()
			End If

			If (KeyHit(KEY_MINUS)) Then
				FlxG.Mute = False
				FlxG.Volume(FlxG.Volume() - .1)
				_ShowSoundTray()
			End If

			If (KeyHit(KEY_EQUALS)) Then
				FlxG.Mute = False
				FlxG.Volume(FlxG.Volume() + .1)
				_ShowSoundTray()
			End If
		End If

		_UpdateSoundTray(_step)
	#End

		Return 0
	End Method

	Method OnRender:Int()
	#If FLX_DEBUG_ENABLED = "1"
		FlxBasic._VisibleCount = 0
	#End

	#If TARGET = "html5" And FLX_WEBGL_ENABLED = "0"

		If ( Not _stateIsReady) Then
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)
			_state.Create()
			_stateIsReady = True
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)

			Return 0
		End If

	#End
		_Draw()
		Return 0
	End Method

	Method OnSuspend:Int()
		ShowMouse()
		FlxG.PauseSounds()
		Return 0
	End Method

	Method OnResume:Int()
	#If FLX_DEBUG_ENABLED = "1"
		If (Not _debuggerUp And Not useSystemCursor) Then
			HideMouse()
		End If
	#Else
		If ( Not useSystemCursor) Then
			HideMouse()
		End If
	#End

		FlxG.ResetInput()
		FlxG.ResumeSounds()
		Return 0
	End Method

	Method OnBack:Int()
		If (_state = Null Or _state.DoBack()) Then
			Return Super.OnBack()
		End If

		Return 0
	End Method

	Method OnClose:Int()
		If (_state = Null Or _state.DoClose()) Then
			Return Super.OnClose()
		End If

		Return 0
	End Method

	Method SetSwitchStateListener:Void(listener:FlxSwitchStateListener)
		_switchStateListener = listener
	End Method

	Method OnContentInit:Void()
	End Method

Private
	Method _ShowSoundTray:Void(silent:Bool = False)
		If (Not silent) Then
			FlxG.Play("beep" + FlxG.DATA_SUFFIX)
		End If

		_soundTrayTimer = 1
		_soundTrayY = 0
		_soundTrayVisible = True
	End Method

	Method _SwitchState:Void()
		FlxG.ResetCameras()
		FlxG.ResetInput()
		FlxG.DestroySounds()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debugger <> Null) Then
			'note: TODO:!
		End If
	#End

		Local timeManager:TimerManager = FlxTimer.Manager()
		If (timeManager <> Null) timeManager.Clear()

		If (_state <> Null) _state.Destroy()
		If (_switchStateListener <> Null) _switchStateListener.OnSwitchState(_state, _requestedState)

		_state = _requestedState

	#If TARGET = "html5" And FLX_WEBGL_ENABLED = "0"

		_stateIsReady = False

	#Else

		BeginRender()
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)
			_state.Create()
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)
		EndRender()

	#End

		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If
	End Method

	Method _Step:Void()
		If (_requestedReset) Then
			_Reset()
		End If

		If (_recordingRequested) Then
			_recordingRequested = False
			_replay.Create(FlxG.GlobalSeed)
			_recording = True

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
				FlxG.Log("FLIXEL: starting new flixel gameplay record.")
			End If
		#End

		ElseIf (_replayRequested)
			_replayRequested = False
			_replay.Rewind()
			FlxG.GlobalSeed = _replay.seed

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

			_replaying = True
		End If

	#If TARGET = "html5" And FLX_WEBGL_ENABLED = "0"

		If (_state <> _requestedState) Then
			_SwitchState()
			Return
		End If

		If ( Not _stateIsReady) Return

	#Else

		If (_state <> _requestedState) _SwitchState()

	#end

	#If FLX_DEBUG_ENABLED = "1"
		FlxBasic._ActiveCount = 0
	#End

		If (_replaying) Then
			If (_replayCancelKeys.Length() > 0 And Not KeyDown(192) And Not KeyDown(220)) Then
				Local i:Int = 0
				Local l:Int = _replayCancelKeys.Length()

				While (i < l)
					If (KeyDown(_replayCancelKeys[i])) Then
						If (_replayCallback <> Null) Then
							_replayCallback.OnReplayComplete()
							_replayCallback = Null
						Else
							FlxG.StopReplay()
						End If

						Exit
					End If

					i += 1
				Wend
			End If

			_replay.PlayNextFrame()

			If (_replayTimer > 0) Then
				_replayTimer -= _step

				If (_replayTimer <= 0) Then
					If (_replayCallback <> Null) Then
						_replayCallback.OnReplayComplete()
						_replayCallback = Null
					Else
						FlxG.StopReplay()
					End If
				End If
			End If

			If (_replaying And _replay.finished) Then
				FlxG.StopReplay()

				If (_replayCallback <> Null) Then
					_replayCallback.OnReplayComplete()
					_replayCallback = Null
				End If
			End If

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

		Else
			FlxG.UpdateInput()
		End If

		If (_recording) Then
			_replay.RecordFrame()

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

		End If

		_Update()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debugger <> Null) Then
			'note: TODO:
		End If
	#End
	End Method

	Method _Update:Void()
		FlxG.Elapsed = FlxG.TimeScale * (_step / 1000.0)

		FlxG.UpdateSounds()
		FlxG.UpdatePlugins()
		_state.DoUpdate()

		If (FlxG.Tweener.active And FlxG.Tweener.HasTween) Then
			FlxG.Tweener.UpdateTweens()
		End If

		FlxG.UpdateCameras()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debuggerUp) Then
			'note: TODO:!
		End If
	#End
	End Method

	Method _UpdateSoundTray:Void(ms:Int)
		If (Not _soundTrayVisible) Return

		If (_soundTrayTimer > 0) Then
			_soundTrayTimer -= ms / 1000.0

		ElseIf (_soundTrayY > -_soundTrayHeight)
			_soundTrayY =_soundTrayY - (ms / 1000.0 * FlxG.Height * 2)

			If (_soundTrayY <= -_soundTrayHeight) Then
				_soundTrayVisible = False

				'note: TODO: Save sound settings
			End If
		End If
	End Method

	Method _Draw:Void()
		Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm" And TARGET <> "win8"
		If( Not FlxG.Mobile) Then
			PushMatrix()
		End If
	#End

		Translate(FlxG._DeviceOffsetX, FlxG._DeviceOffsetY)
		Scale(FlxG._DeviceScaleFactorX, FlxG._DeviceScaleFactorY)

		FlxG._LastDrawingColor = FlxG.WHITE
		FlxG._LastDrawingBlend = GetBlend()
		FlxG._LastDrawingAlpha = GetAlpha()
		FlxG._CurrentCamera = Null

		Local i:Int = 0
		Local l:Int = FlxG.Cameras.Length()

		While(i < l)
			FlxG._CurrentCamera = FlxG.Cameras.Get(i)

			If (FlxG._CurrentCamera <> Null And FlxG._CurrentCamera.active And FlxG._CurrentCamera.exists And FlxG._CurrentCamera.visible) Then
				FlxG._CurrentCamera.DrawFX() 'not really draw. Only calculation
				FlxG._CurrentCamera.Lock()
				_state.DoDraw()
				FlxG.DrawPlugins()
				FlxG._CurrentCamera.Unlock()
			End If

			i+=1
		Wend

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm" And TARGET <> "win8"
		If( Not FlxG.Mobile) Then
			PopMatrix()
			_DrawSoundTray()
			FlxG.Mouse.Draw()
		End If
	#End
	End Method

	Method _DrawSoundTray:Void()
		If (_soundTrayVisible) Then
			Local globalVolume:Int = FlxU.Round(FlxG.Volume() * 10)
			If (FlxG.Mute) globalVolume = 0

			PushMatrix()

			Transform(FlxG._DeviceScaleFactorX, 0, 0, FlxG._DeviceScaleFactorY, ( (FlxG.DeviceWidth - _soundTrayWidth * FlxG._DeviceScaleFactorX) * 0.5), _soundTrayY)

			If (FlxG._LastDrawingAlpha <> .5) Then
				SetAlpha(.5)
				FlxG._LastDrawingAlpha = .5
			End If

			If (FlxG._LastDrawingColor <> FlxG.BLACK) Then
				SetColor(0, 0, 0)
			End If

			DrawRect(0, 0, _soundTrayWidth, _soundTrayHeight)

			SetColor(255, 255, 255)
			FlxG._LastDrawingColor = FlxG.WHITE

			Local bx:Int = 20
			Local by:Int = 28

			Local i:Int = 0
			While (i < 10)
				If (i < globalVolume) Then
					If (FlxG._LastDrawingAlpha <> 1) Then
						SetAlpha(1)
						FlxG._LastDrawingAlpha = 1
					End If

				Else
					If (FlxG._LastDrawingAlpha <> .5) Then
						SetAlpha(.5)
						FlxG._LastDrawingAlpha = .5
					End If
				End If

				DrawRect(bx, by, 8, i * 2)

				bx += 12
				by -= 2
				i += 1
			Wend

			'_soundTrayLabel.Draw()

			PopMatrix()
		End If
	End Method

	Method _Reset:Void()
		_requestedReset = False
		_requestedState = FlxState(_iState.NewInstance())
		_replayTimer = 0
		_replayCancelKeys =[]

		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If

		Local date:Int[] = GetDate()

		Seed = (date[3] * 3600 + date[4] * 60 + date[5]) * 1000 + date[6]
		FlxG.UpdateDevice()

		FlxG.Reset()
	End Method

	Method _ResetFramerate:Void()
		SetUpdateRate(FlxG.Updaterate)
		_step = 1000 / FlxG.Updaterate
		_updaterate = FlxG.Updaterate
	End Method

	Method _InitData:Void()
		FlxAssetsManager.Init()

		Local minSystemFontSize:Int = 8
		Local maxSystemFontSize:Int = 24
		Local fontPathPrefix:String = FlxText.SYSTEM_FONT + "_font_"

		Local system:FlxFont = 	FlxAssetsManager.AddFont(FlxText.SYSTEM_FONT)

		For Local size:Int = minSystemFontSize To maxSystemFontSize
			system.SetPath(size, fontPathPrefix + size + FlxG.DATA_SUFFIX)
		Next

		FlxAssetsManager.AddImage("default" + FlxG.DATA_SUFFIX, "default" +  FlxG.DATA_SUFFIX + ".png")
		FlxAssetsManager.AddImage("button" + FlxG.DATA_SUFFIX, "button" + FlxG.DATA_SUFFIX + ".png")
		FlxAssetsManager.AddImage(FlxTilemap.AUTOTILES, FlxTilemap.AUTOTILES + ".png")
		FlxAssetsManager.AddImage(FlxTilemap.AUTOTILES_ALT, FlxTilemap.AUTOTILES_ALT + ".png")
		FlxAssetsManager.AddCursor("cursor" + FlxG.DATA_SUFFIX, "cursor" + FlxG.DATA_SUFFIX + ".png")

		FlxAssetsManager.AddSound("beep" + FlxG.DATA_SUFFIX, "beep" + FlxG.DATA_SUFFIX + "." + FlxSound.GetValidExt())

		Local contentInitTrigger:FunctionInfo

		For Local classInfo:ClassInfo = EachIn GetClasses()
			contentInitTrigger = classInfo.GetFunction("OnContentInit",[], False)
			If (contentInitTrigger <> Null) contentInitTrigger.Invoke([])
		Next

		Self.OnContentInit()
	End Method
End Class

Strict

Import mojo
Import reflection

Import flxextern
Import flxbasic
Import flxstate
Import flxcamera
Import flxtimer
Import flxtext
Import flxg
Import flxu
Import flxtilemap
Import system.flxassetsmanager
Import system.flxfont
Import system.flxdebugger
Import system.flxreplay
Import plugin.timermanager

#If TARGET = "xna" Or TARGET = "psm" Or TARGET = "win8"
	Import "data/beep_flx.wav"
#ElseIf TARGET = "html5"
	Import "data/beep_flx.ogg"
	Import "data/beep_flx.mp3"
#ElseIf TARGET = "glfw" Or TARGET = "bmax"
	Import "data/beep_flx.ogg"
#Else
	Import "data/beep_flx.mp3"
#End

Class FlxGame extends App

	Field useSoundHotKeys:Bool

	Field useSystemCursor:Bool

	Field forceDebugger:Bool

	Field _state:FlxState

	Field _requestedState:FlxState

	Field _requestedReset:Bool

	Field _debugger:FlxDebugger

	Field _debuggerUp:Bool

	Field _replay:FlxReplay

	Field _replayRequested:Bool

	Field _recordingRequested:Bool

	Field _replaying:Bool

	Field _recording:Bool

	Field _replayCancelKeys:Int[]

	Field _replayTimer:Float

	Field _replayCallback:FlxReplayListener

Private
	Field _iState:ClassInfo

	Field _step:Int

	Field _soundTrayTimer:Float

	Field _soundTrayWidth:Float

	Field _soundTrayHeight:Float

	Field _soundTrayX:Float

	Field _soundTrayY:Float

	Field _soundTrayVisible:Bool

	Field _soundTrayLabel:FlxText

	Field _updaterate:Int

	Field _switchStateListener:FlxSwitchStateListener

Public
	Method New(gameSizeX:Int, gameSizeY:Int, initialState:ClassInfo, zoom:Float = 1, updaterate:Int = 60, useSystemCursor:Bool = False)
		Local classObject:GlobalInfo

		For Local classInfo:ClassInfo = EachIn GetClasses()
			classObject = classInfo.GetGlobal("__CLASS__", False)

			If (classObject <> Null) Then
				classObject.SetValue(classInfo)
			End If
		Next

		_soundTrayTimer = 0
		_soundTrayWidth = 160
		_soundTrayHeight = 45
		_soundTrayX = 0
		_soundTrayY	= -_soundTrayHeight
		_soundTrayVisible = False

		FlxG.Init(Self, gameSizeX, gameSizeY, zoom)
		FlxG.Updaterate = updaterate

		_state = Null

		useSoundHotKeys = Not FlxG.Mobile
		Self.useSystemCursor = useSystemCursor

		If (Not useSystemCursor) HideMouse()

		forceDebugger = False
		_debuggerUp = False

		_replay = New FlxReplay()
		_replayRequested = False
		_recordingRequested = False
		_replaying = False
		_recording = False

		_iState = initialState
		_requestedState = Null
		_requestedReset = True
	End Method

	Method OnCreate:Int()
		_InitData()
		_Reset()

		#Rem
		_soundTrayLabel = New FlxText(10, 32, _soundTrayWidth, "VOLUME")
		_soundTrayLabel.SetFormat(FlxText.SYSTEM_FONT, 16, FlxG.WHITE, FlxText.ALIGN_CENTER)
		#End
		Return 0
	End Method

	Method OnUpdate:Int()
		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If

	#If FLX_ASYNC_EVENTS_ENABLED = "1"
		UpdateAsyncEvents()
	#End

		FlxG.UpdateDevice()
		_Step()

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm"
		If (useSoundHotKeys) Then
			If (KeyHit(KEY_0)) Then
				FlxG.Mute = Not FlxG.Mute

				If (FlxG.VolumeHandler <> Null) Then
					If (FlxG.Mute) Then
						FlxG.VolumeHandler.OnVolumeChange(0)
					Else
						FlxG.VolumeHandler.OnVolumeChange(FlxG.Volume())
					End If
				End If

				_ShowSoundTray()
			End If

			If (KeyHit(KEY_MINUS)) Then
				FlxG.Mute = False
				FlxG.Volume(FlxG.Volume() - .1)
				_ShowSoundTray()
			End If

			If (KeyHit(KEY_EQUALS)) Then
				FlxG.Mute = False
				FlxG.Volume(FlxG.Volume() + .1)
				_ShowSoundTray()
			End If
		End If

		_UpdateSoundTray(_step)
	#End

		Return 0
	End Method

	Method OnRender:Int()
	#If FLX_DEBUG_ENABLED = "1"
		FlxBasic._VisibleCount = 0
	#End
		_Draw()
		Return 0
	End Method

	Method OnSuspend:Int()
		ShowMouse()
		FlxG.PauseSounds()
		Return 0
	End Method

	Method OnResume:Int()
	#If FLX_DEBUG_ENABLED = "1"
		If (Not _debuggerUp And Not useSystemCursor) Then
			HideMouse()
		End If
	#Else
		If ( Not useSystemCursor) Then
			HideMouse()
		End If
	#End

		FlxG.ResetInput()
		FlxG.ResumeSounds()
		Return 0
	End Method

	Method OnBack:Int()
		If (_state = Null Or _state.DoBack()) Then
			Return Super.OnBack()
		End If

		Return 0
	End Method

	Method OnClose:Int()
		If (_state = Null Or _state.DoClose()) Then
			Return Super.OnClose()
		End If

		Return 0
	End Method

	Method SetSwitchStateListener:Void(listener:FlxSwitchStateListener)
		_switchStateListener = listener
	End Method

	Method OnContentInit:Void()
	End Method

Private
	Method _ShowSoundTray:Void(silent:Bool = False)
		If (Not silent) Then
			FlxG.Play("beep" + FlxG.DATA_SUFFIX)
		End If

		_soundTrayTimer = 1
		_soundTrayY = 0
		_soundTrayVisible = True
	End Method

	Method _SwitchState:Void()
		FlxG.ResetCameras()
		FlxG.ResetInput()
		FlxG.DestroySounds()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debugger <> Null) Then
			'note: TODO:!
		End If
	#End

		Local timeManager:TimerManager = FlxTimer.Manager()
		If (timeManager <> Null) timeManager.Clear()

		If (_state <> Null) _state.Destroy()
		If (_switchStateListener <> Null) _switchStateListener.OnSwitchState(_state, _requestedState)

		_state = _requestedState

		BeginRender()
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)
			_state.Create()
			Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)
		EndRender()

		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If
	End Method

	Method _Step:Void()
		If (_requestedReset) Then
			_Reset()
		End If

		If (_recordingRequested) Then
			_recordingRequested = False
			_replay.Create(FlxG.GlobalSeed)
			_recording = True

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
				FlxG.Log("FLIXEL: starting new flixel gameplay record.")
			End If
		#End

		ElseIf (_replayRequested)
			_replayRequested = False
			_replay.Rewind()
			FlxG.GlobalSeed = _replay.seed

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

			_replaying = True
		End If

		If (_state <> _requestedState) _SwitchState()

	#If FLX_DEBUG_ENABLED = "1"
		FlxBasic._ActiveCount = 0
	#End

		If (_replaying) Then
			If (_replayCancelKeys.Length() > 0 And Not KeyDown(192) And Not KeyDown(220)) Then
				Local i:Int = 0
				Local l:Int = _replayCancelKeys.Length()

				While (i < l)
					If (KeyDown(_replayCancelKeys[i])) Then
						If (_replayCallback <> Null) Then
							_replayCallback.OnReplayComplete()
							_replayCallback = Null
						Else
							FlxG.StopReplay()
						End If

						Exit
					End If

					i += 1
				Wend
			End If

			_replay.PlayNextFrame()

			If (_replayTimer > 0) Then
				_replayTimer -= _step

				If (_replayTimer <= 0) Then
					If (_replayCallback <> Null) Then
						_replayCallback.OnReplayComplete()
						_replayCallback = Null
					Else
						FlxG.StopReplay()
					End If
				End If
			End If

			If (_replaying And _replay.finished) Then
				FlxG.StopReplay()

				If (_replayCallback <> Null) Then
					_replayCallback.OnReplayComplete()
					_replayCallback = Null
				End If
			End If

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

		Else
			FlxG.UpdateInput()
		End If

		If (_recording) Then
			_replay.RecordFrame()

		#If FLX_DEBUG_ENABLED = "1"
			If (_debugger <> Null) Then
				'note: TODO:
			End If
		#End

		End If

		_Update()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debugger <> Null) Then
			'note: TODO:
		End If
	#End
	End Method

	Method _Update:Void()
		FlxG.Elapsed = FlxG.TimeScale * (_step / 1000.0)

		FlxG.UpdateSounds()
		FlxG.UpdatePlugins()
		_state.DoUpdate()

		If (FlxG.Tweener.active And FlxG.Tweener.HasTween) Then
			FlxG.Tweener.UpdateTweens()
		End If

		FlxG.UpdateCameras()

	#If FLX_DEBUG_ENABLED = "1"
		If (_debuggerUp) Then
			'note: TODO:!
		End If
	#End
	End Method

	Method _UpdateSoundTray:Void(ms:Int)
		If (Not _soundTrayVisible) Return

		If (_soundTrayTimer > 0) Then
			_soundTrayTimer -= ms / 1000.0

		ElseIf (_soundTrayY > -_soundTrayHeight)
			_soundTrayY =_soundTrayY - (ms / 1000.0 * FlxG.Height * 2)

			If (_soundTrayY <= -_soundTrayHeight) Then
				_soundTrayVisible = False

				'note: TODO: Save sound settings
			End If
		End If
	End Method

	Method _Draw:Void()
		Cls(FlxG._BgColor.r, FlxG._BgColor.g, FlxG._BgColor.b)

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm" And TARGET <> "win8"
		If( Not FlxG.Mobile) Then
			PushMatrix()
		End If
	#End

		Translate(FlxG._DeviceOffsetX, FlxG._DeviceOffsetY)
		Scale(FlxG._DeviceScaleFactorX, FlxG._DeviceScaleFactorY)

		FlxG._LastDrawingColor = FlxG.WHITE
		FlxG._LastDrawingBlend = GetBlend()
		FlxG._LastDrawingAlpha = GetAlpha()
		FlxG._CurrentCamera = Null

		Local i:Int = 0
		Local l:Int = FlxG.Cameras.Length()

		While(i < l)
			FlxG._CurrentCamera = FlxG.Cameras.Get(i)

			If (FlxG._CurrentCamera <> Null And FlxG._CurrentCamera.active And FlxG._CurrentCamera.exists And FlxG._CurrentCamera.visible) Then
				FlxG._CurrentCamera.DrawFX() 'not really draw. Only calculation
				FlxG._CurrentCamera.Lock()
				_state.DoDraw()
				FlxG.DrawPlugins()
				FlxG._CurrentCamera.Unlock()
			End If

			i+=1
		Wend

	#If TARGET <> "ios" And TARGET <> "android" And TARGET <> "psm" And TARGET <> "win8"
		If( Not FlxG.Mobile) Then
			PopMatrix()
			_DrawSoundTray()
			FlxG.Mouse.Draw()
		End If
	#End
	End Method

	Method _DrawSoundTray:Void()
		If (_soundTrayVisible) Then
			Local globalVolume:Int = FlxU.Round(FlxG.Volume() * 10)
			If (FlxG.Mute) globalVolume = 0

			PushMatrix()

			Transform(FlxG._DeviceScaleFactorX, 0, 0, FlxG._DeviceScaleFactorY, ( (FlxG.DeviceWidth - _soundTrayWidth * FlxG._DeviceScaleFactorX) * 0.5), _soundTrayY)

			If (FlxG._LastDrawingAlpha <> .5) Then
				SetAlpha(.5)
				FlxG._LastDrawingAlpha = .5
			End If

			If (FlxG._LastDrawingColor <> FlxG.BLACK) Then
				SetColor(0, 0, 0)
			End If

			DrawRect(0, 0, _soundTrayWidth, _soundTrayHeight)

			SetColor(255, 255, 255)
			FlxG._LastDrawingColor = FlxG.WHITE

			Local bx:Int = 20
			Local by:Int = 28

			Local i:Int = 0
			While (i < 10)
				If (i < globalVolume) Then
					If (FlxG._LastDrawingAlpha <> 1) Then
						SetAlpha(1)
						FlxG._LastDrawingAlpha = 1
					End If

				Else
					If (FlxG._LastDrawingAlpha <> .5) Then
						SetAlpha(.5)
						FlxG._LastDrawingAlpha = .5
					End If
				End If

				DrawRect(bx, by, 8, i * 2)

				bx += 12
				by -= 2
				i += 1
			Wend

			'_soundTrayLabel.Draw()

			PopMatrix()
		End If
	End Method

	Method _Reset:Void()
		_requestedReset = False
		_requestedState = FlxState(_iState.NewInstance())
		_replayTimer = 0
		_replayCancelKeys =[]

		If (FlxG.Updaterate <> _updaterate) Then
			_ResetFramerate()
		End If

		Local date:Int[] = GetDate()

		Seed = (date[3] * 3600 + date[4] * 60 + date[5]) * 1000 + date[6]
		FlxG.UpdateDevice()

		FlxG.Reset()
	End Method

	Method _ResetFramerate:Void()
		SetUpdateRate(FlxG.Updaterate)
		_step = 1000 / FlxG.Updaterate
		_updaterate = FlxG.Updaterate
	End Method

	Method _InitData:Void()
		FlxAssetsManager.Init()

		Local minSystemFontSize:Int = 8
		Local maxSystemFontSize:Int = 24
		Local fontPathPrefix:String = FlxText.SYSTEM_FONT + "_font_"

		Local system:FlxFont = 	FlxAssetsManager.AddFont(FlxText.SYSTEM_FONT)

		For Local size:Int = minSystemFontSize To maxSystemFontSize
			system.SetPath(size, fontPathPrefix + size + FlxG.DATA_SUFFIX)
		Next

		FlxAssetsManager.AddImage("default" + FlxG.DATA_SUFFIX, "default" +  FlxG.DATA_SUFFIX + ".png")
		FlxAssetsManager.AddImage("button" + FlxG.DATA_SUFFIX, "button" + FlxG.DATA_SUFFIX + ".png")
		FlxAssetsManager.AddImage(FlxTilemap.AUTOTILES, FlxTilemap.AUTOTILES + ".png")
		FlxAssetsManager.AddImage(FlxTilemap.AUTOTILES_ALT, FlxTilemap.AUTOTILES_ALT + ".png")
		FlxAssetsManager.AddCursor("cursor" + FlxG.DATA_SUFFIX, "cursor" + FlxG.DATA_SUFFIX + ".png")

		FlxAssetsManager.AddSound("beep" + FlxG.DATA_SUFFIX, "beep" + FlxG.DATA_SUFFIX + "." + FlxSound.GetValidExt())

		Local contentInitTrigger:FunctionInfo

		For Local classInfo:ClassInfo = EachIn GetClasses()
			contentInitTrigger = classInfo.GetFunction("OnContentInit",[], False)
			If (contentInitTrigger <> Null) contentInitTrigger.Invoke([])
		Next

		Self.OnContentInit()
	End Method
End Class


Fixed GUI example

Fixed GUI example

Strict

Import diddy

Function Main:Int()
	game = new MyGame()
	Return 0
End Function

Global guiScreen:GUIScreen

Class MyGame extends DiddyApp
	Method OnCreate:Int()
		Super.OnCreate()
		images.Load("button.png", "", False)
		images.Load("buttonClick.png", "", False)
		images.Load("check.png", "", False)
		images.Load("checkClick.png", "", False)
		drawFPSOn = True
		guiScreen = new GUIScreen
		guiScreen.PreStart()
		return 0
	End
End

Class GUIScreen Extends Screen
	Field mygui:MyGUI = New MyGUI

	Method New()
		name = "Game"
	End

	Method Start:Void()
		game.screenFade.Start(50, false)

	End

	Method Render:Void()
		Cls
		mygui.Draw()
		SetColor(255, 255, 255)
		DrawText("value="+mygui.slider.value, 0, 20)
	End

	Method Update:Void()
		mygui.Update()
		If KeyHit(KEY_ESCAPE)
			game.screenFade.Start(50, true)
			game.nextScreen = game.exitScreen
		End
	End

End

Class MyGUI Extends GUI
	Field button:Button
	Field toggleButton:Button
	Field slider:Slider
	Field buttonImage:GameImage

	Method New()
		button = New Button(desktop, game.images.Find("button"), game.images.Find("buttonClick"))
		button.SetBounds(150,50,100,50)
		button.Text ("HELLO", 0.5, 0.5)

		toggleButton = New Button(desktop, game.images.Find("check"))
		toggleButton.toggle = True
		toggleButton.SetBounds(50,120,100,50)
		toggleButton.StyleSelected.image = game.images.Find("checkClick")
		toggleButton.StyleSelected.drawBackground = False
	'	toggleButton.StyleNormal = cs
'		toggleButton.StyleNormal.red = 0
'		toggleButton.StyleNormal.green = 0
'		toggleButton.StyleNormal.blue = 255
'		toggleButton.StyleSelected.red = 0
'		toggleButton.StyleSelected.green = 255
'		toggleButton.StyleSelected.blue = 0

		slider = New Slider(desktop)
		slider.SetBounds(50,190,200,20)
		slider.ShowButtons = True
		slider.StyleNormal.red = 0
		slider.StyleNormal.green = 0
		slider.StyleNormal.blue = 255

	End

	Method ActionPerformed:Void(source:Component, action:String)
		If source = slider And action = ACTION_VALUE_CHANGED Then
			Print "slider="+Slider(source).value
		ElseIf source = button And action = ACTION_CLICKED Then
			Print("pressed button!")
		ElseIf source = toggleButton And action = ACTION_CLICKED Then
			If toggleButton.selected Then
				Print("pressed toggleButton! selected=true")
			Else
				Print("pressed toggleButton! selected=false")
			End
		End
	End
End

Strict

Import diddy

Function Main:Int()
	game = new MyGame()
	Return 0
End Function

Global guiScreen:GUIScreen

Class MyGame extends DiddyApp
	Method OnCreate:Int()
		Super.OnCreate()
		images.Load("button.png", "", False)
		images.Load("buttonClick.png", "", False)
		images.Load("check.png", "", False)
		images.Load("checkClick.png", "", False)
		drawFPSOn = True
		guiScreen = new GUIScreen
		guiScreen.PreStart()
		return 0
	End
End

Class GUIScreen Extends Screen
	Field mygui:MyGUI = New MyGUI

	Method New()
		name = "Game"
	End

	Method Start:Void()
		game.screenFade.Start(50, false)

	End

	Method Render:Void()
		Cls
		mygui.Draw()
		SetColor(255, 255, 255)
		DrawText("value="+mygui.slider.value, 0, 20)
	End

	Method Update:Void()
		mygui.Update()
		If KeyHit(KEY_ESCAPE)
			game.screenFade.Start(50, true)
			game.nextScreen = game.exitScreen
		End
	End

End

Class MyGUI Extends GUI
	Field button:Button
	Field toggleButton:Button
	Field slider:Slider
	Field buttonImage:GameImage

	Method New()
		button = New Button(desktop, game.images.Find("button"), game.images.Find("buttonClick"))
		button.SetBounds(150,50,100,50)
		button.text = "HELLO"

		toggleButton = New Button(desktop, game.images.Find("check"))
		toggleButton.toggle = True
		toggleButton.SetBounds(50,120,100,50)
		toggleButton.StyleSelected.image = game.images.Find("checkClick")
		toggleButton.StyleSelected.drawBackground = False
	'	toggleButton.StyleNormal = cs
'		toggleButton.StyleNormal.red = 0
'		toggleButton.StyleNormal.green = 0
'		toggleButton.StyleNormal.blue = 255
'		toggleButton.StyleSelected.red = 0
'		toggleButton.StyleSelected.green = 255
'		toggleButton.StyleSelected.blue = 0

		slider = New Slider(desktop)
		slider.SetBounds(50,190,200,20)
		slider.ShowButtons = True
		slider.StyleNormal.red = 0
		slider.StyleNormal.green = 0
		slider.StyleNormal.blue = 255

	End

	Method ActionPerformed:Void(source:Component, action:String)
		If source = slider And action = ACTION_VALUE_CHANGED Then
			Print "slider="+Slider(source).value
		ElseIf source = button And action = ACTION_CLICKED Then
			Print("pressed button!")
		ElseIf source = toggleButton And action = ACTION_CLICKED Then
			If toggleButton.selected Then
				Print("pressed toggleButton! selected=true")
			Else
				Print("pressed toggleButton! selected=false")
			End
		End
	End
End


StoryboardElement's and StoryboardEffect's Update methods now accept the Storyboard as the first argument.

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import functions
Import collections
Import xml
Import format

Public
Import exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 6

Class Storyboard
Private
	Field sprites:ArrayList<StoryboardSprite> = New ArrayList<StoryboardSprite>
	Field sounds:ArrayList<StoryboardSound> = New ArrayList<StoryboardSound>
	Field musics:ArrayList<StoryboardMusic> = New ArrayList<StoryboardMusic>
	Field effects:ArrayList<StoryboardEffect> = New ArrayList<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String)
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile("storyboard.xml")
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Add(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Add(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Add(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Add(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:ArrayList<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:ArrayList<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:ArrayList<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:ArrayList<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Size
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(Self, currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Size
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(Self, currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Size
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(Self, currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Size
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(Self, currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Size
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Size
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

Public
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	Method New()
		id = nextId
		nextId += 1
	End

	Method Compare:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Equals:Bool(other:Object)
		Return Compare(other)=0
	End

	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:ArrayList<StoryboardSpriteKeyframe> = New ArrayList<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Add(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method Compare:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.Compare(other)
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Size
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method Compare:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

	Method Equals:Bool(other:Object)
		Return Compare(other)=0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import functions
Import collections
Import xml
Import format

Public
Import exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 6

Class Storyboard
Private
	Field sprites:ArrayList<StoryboardSprite> = New ArrayList<StoryboardSprite>
	Field sounds:ArrayList<StoryboardSound> = New ArrayList<StoryboardSound>
	Field musics:ArrayList<StoryboardMusic> = New ArrayList<StoryboardMusic>
	Field effects:ArrayList<StoryboardEffect> = New ArrayList<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String)
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile("storyboard.xml")
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Add(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Add(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Add(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Add(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:ArrayList<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:ArrayList<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:ArrayList<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:ArrayList<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Size
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Size
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Size
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Size
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Size
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Size
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

Public
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	Method New()
		id = nextId
		nextId += 1
	End

	Method Compare:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Equals:Bool(other:Object)
		Return Compare(other)=0
	End

	Method Update:Void(currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:ArrayList<StoryboardSpriteKeyframe> = New ArrayList<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Add(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method Compare:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.Compare(other)
	End

	Method Update:Void(currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Size
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method Compare:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

	Method Equals:Bool(other:Object)
		Return Compare(other)=0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]


FlxTween type is a bit flags now #46

FlxTween type is a bit flags now #46

Strict

Import flixel.flxbasic
Import flixel.flxg
Import util.ease

Class FlxTween

	Const PERSIST:Int = 1

	Const LOOPING:Int = 2

	Const TOANDFRO:Int = 4

	Const ONESHOT:Int = 8

	Const BACKWARD:Int = 16

	Field active:Bool

	Field complete:FlxTweenListener

	Field _parent:FlxBasic

	Field _finish:Bool

	Field _target:Float

	Field _ease:FlxEaseFunction

	Field _type:Int

	Field _t:Float

	Field _time:Float

	Field _backward:Bool

	Field _times:Int

Public
	Method New(duration:Float, type:Int = -1, complete:FlxTweenListener = Null, ease:FlxEaseFunction = Null)
		_target = duration

		_type = type
		If (_type < 0) _type = FlxTween.PERSIST

		Self.complete = complete
		_ease = ease
		_t = 0

		_backward = Bool(type & BACKWARD)
	End Method

	Method Destroy:Void()
		complete = Null
		_parent = Null
		_ease = Null
	End Method

	Method Update:Void()
		_time += FlxG.Elapsed
		_t = _time / _target

		If (_ease <> Null) _t = _ease.Ease(_t)
		If (_backward) _t = 1 - _t

		If (_time >= _target) Then
			If ( Not _backward) Then
				_t = 1
			Else
				_t = 0
			End If
			_finish = True
		End If
	End Method

	Method Start:Void()
		_time = 0

		If (_target = 0) Then
			active = False
			Return
		End If

		active = True
	End Method

	Method Finish:Void()
		Select( (_type & ~ BACKWARD))
			Case PERSIST
				_time = _target
				active = False

			Case LOOPING
				_time Mod = _target
				_t = _time / _target

				If (_ease <> Null And _t > 0 And _t < 1) Then
					_t = _ease.Ease(_t)
				End If

				Start()

			Case TOANDFRO
				_time Mod = _target
				_t = _time / _target

				If (_ease <> Null And _t > 0 And _t < 1) Then
					_t = _ease.Ease(_t)
				End If

				If (_backward) _t = 1 - _t
				_backward = Not _backward

				Start()

			Case ONESHOT
				_time = _target
				active = False
				_parent.RemoveTween(Self, True)
		End Select

		_finish = False
		If (complete <> Null) complete.OnTweenComplete()
	End Method

	Method Percent:Float() Property
		Return _time / _target
	End Method

	Method Percent:Void(value:Float) Property
		_time = _target * value
	End Method

	Method Scale:Float() Property
		Return _t
	End Method

End Class

Interface FlxTweenListener

	Method OnTweenComplete:Void()

End Interface

Strict

Import flixel.flxbasic
Import flixel.flxg
Import util.ease

Class FlxTween

	Const PERSIST:Int = 0

	Const BACKWARD:Int = 1

	Const LOOPING:Int = 2

	Const TOANDFRO:Int = 3

	Const ONESHOT:Int = 4

	Const ONESHOTBACKWARD:Int = 5

	Field active:Bool

	Field complete:FlxTweenListener

	Field _parent:FlxBasic

	Field _finish:Bool

	Field _target:Float

	Field _ease:FlxEaseFunction

	Field _type:Int

	Field _t:Float

	Field _time:Float

	Field _backward:Bool

	Field _times:Int

Public
	Method New(duration:Float, type:Int = -1, complete:FlxTweenListener = Null, ease:FlxEaseFunction = Null)
		_target = duration

		_type = type
		If (_type < 0) _type = FlxTween.PERSIST

		Self.complete = complete
		_ease = ease
		_t = 0

		_backward = (type = BACKWARD Or type = ONESHOTBACKWARD)
	End Method

	Method Destroy:Void()
		complete = Null
		_parent = Null
		_ease = Null
	End Method

	Method Update:Void()
		_time += FlxG.Elapsed
		_t = _time / _target

		If (_ease <> Null) _t = _ease.Ease(_t)
		If (_backward) _t = 1 - _t

		If (_time >= _target) Then
			If ( Not _backward) Then
				_t = 1
			Else
				_t = 0
			End If
			_finish = True
		End If
	End Method

	Method Start:Void()
		_time = 0

		If (_target = 0) Then
			active = False
			Return
		End If

		active = True
	End Method

	Method Finish:Void()
		Select(_type)
			Case PERSIST, BACKWARD
				_time = _target
				active = False

			Case LOOPING
				_time Mod = _target
				_t = _time / _target

				If (_ease <> Null And _t > 0 And _t < 1) Then
					_t = _ease.Ease(_t)
				End If

				Start()

			Case TOANDFRO
				_time Mod = _target
				_t = _time / _target

				If (_ease <> Null And _t > 0 And _t < 1) Then
					_t = _ease.Ease(_t)
				End If

				If (_backward) _t = 1 - _t
				_backward = Not _backward

				Start()

			Case ONESHOT, ONESHOTBACKWARD
				_time = _target
				active = False
				_parent.RemoveTween(Self, True)
		End Select

		_finish = False
		If (complete <> Null) complete.OnTweenComplete()
	End Method

	Method Percent:Float() Property
		Return _time / _target
	End Method

	Method Percent:Void(value:Float) Property
		_time = _target * value
	End Method

	Method Scale:Float() Property
		Return _t
	End Method

End Class

Interface FlxTweenListener

	Method OnTweenComplete:Void()

End Interface


Added another overload for 'SetBuffer', etc.

Strict

Public

' Imports (Public):
Import bitfield
Import compare

' Imports (Private):
Private

Import brl.stream
Import brl.databuffer

Import except

Public

' This command treats data as 8-bit (Turns something like $F7, into $FFFFFFF7)
' This is only applied on little-endian systems,
' please use this command for hex values (Monkey converts hex on compile-time).
Function PaddedHex:UInt(I:UInt)
	If (Not BigEndian()) Then
		Return AsByte(I)
	Endif

	Return I
End

' This command is little-endian only:
Function AsByte:UInt(I:UInt)
	Return ((I Shl 24) Shr 24)
End

' This command is useful when dealing with arrays.
Function OutOfBounds:Bool(Position:UInt, Length:UInt)
	Return (Position >= Length)
End

' This performs a transfer operation from 'InputStream' to
' 'OutputStream', using a block of memory of size 'DataSize'.
Function Transfer:Void(InputStream:Stream, OutputStream:Stream, DataSize:UInt)
	' Check for errors:
	#If Not MONKEYLANG_EXTENSION_TYPE_UNISGNED_INT
		If (DataSize <= 0) Then
			Throw New StreamTransferException(Null, DataSize)
		Endif
	#End

	If (InputStream = Null Or OutputStream = Null) Then
		Throw New StreamUnavailableException()
	End

	' Local variable(s):
	#If UTIL_TEMP_BUFFERS
		Local Data:DataBuffer = New DataBuffer(DataSize)

		If (Data = Null) Then ' Data.Length <> DataSize
			Throw New BulkAllocationException<DataBuffer>(Data, DataSize)
		Endif

		Local DataLength:= DataSize ' Data.Length

		' Read from the input-stream, then write to the output-stream:
		InputStream.ReadAll(Data, 0, DataLength)
		OutputStream.WriteAll(Data, 0, DataLength)

		Data.Discard()
	#Else
		'Local Data:Int[DataSize]
		'Local Data:Byte[DataSize]

		' This is terrible, but it's our only option without temporary buffers.
		For Local I:= 1 To DataSize ' Data.Length
			OutputStream.WriteByte(InputStream.ReadByte())
		Next
	#End

	Return
End

Function ResizeBuffer:DataBuffer(Buffer:DataBuffer, Size:Long=AUTOMATIC_LENGTH, CopyData:Bool=True, DiscardOldBuffer:Bool=False, OnlyWhenDifferentSizes:Bool=False)
	Local BufferAvailable:Bool = (Buffer <> Null)

	If (BufferAvailable And OnlyWhenDifferentSizes) Then
		If (Size <> AUTOMATIC_LENGTH And Buffer.Length = Size) Then
			Return Buffer
		Endif
	Endif

	If (Size = AUTOMATIC_LENGTH) Then
		Size = Buffer.Length
	Endif

	' Allocate a new data-buffer.
	Local B:= New DataBuffer(Size)

	' Copy the buffer's bytes over to 'B'.
	If (BufferAvailable) Then
		If (CopyData) Then
			' Copy the contents of 'Buffer' to the newly generated buffer-object.
			Buffer.CopyBytes(0, B, 0, Buffer.Length)
		Endif

		If (DiscardOldBuffer) Then
			' Discard the old buffer.
			Buffer.Discard()
		Endif
	Endif

	' Return the newly generated buffer.
	Return B
End

Function SetBuffer:Void(Output:DataBuffer, Value:Byte, Count:UInt, Offset:UInt=0)
	For Local I:= Offset Until (Count + Offset)
		Output.PokeByte(I, Value)
	Next

	Return
End

Function SetBuffer:Void(Output:DataBuffer, Value:Byte, Offset:UInt=0)
	SetBuffer(Output, Value, Output.Length - Offset, Offset)

	Return
End

Function SetBuffer:Void(Output:DataBuffer, Bytes:Byte[], Bytes_Length:UInt, Bytes_Offset:UInt=0, Offset:UInt=0)
	For Local I:= 0 Until Bytes_Length
		Output.PokeByte((I + Offset), Bytes[I + Bytes_Offset])
	Next

	Return
End

Strict

Public

' Imports (Public):
Import bitfield
Import compare

' Imports (Private):
Private

Import brl.stream
Import brl.databuffer

Import except

Public

' This command treats data as 8-bit (Turns something like $F7, into $FFFFFFF7)
' This is only applied on little-endian systems,
' please use this command for hex values (Monkey converts hex on compile-time).
Function PaddedHex:UInt(I:UInt)
	If (Not BigEndian()) Then
		Return AsByte(I)
	Endif

	Return I
End

' This command is little-endian only:
Function AsByte:UInt(I:UInt)
	Return ((I Shl 24) Shr 24)
End

' This command is useful when dealing with arrays.
Function OutOfBounds:Bool(Position:UInt, Length:UInt)
	Return (Position >= Length)
End

' This performs a transfer operation from 'InputStream' to
' 'OutputStream', using a block of memory of size 'DataSize'.
Function Transfer:Void(InputStream:Stream, OutputStream:Stream, DataSize:UInt)
	' Check for errors:
	#If Not MONKEYLANG_EXTENSION_TYPE_UNISGNED_INT
		If (DataSize <= 0) Then
			Throw New StreamTransferException(Null, DataSize)
		Endif
	#End

	If (InputStream = Null Or OutputStream = Null) Then
		Throw New StreamUnavailableException()
	End

	' Local variable(s):
	#If UTIL_TEMP_BUFFERS
		Local Data:DataBuffer = New DataBuffer(DataSize)

		If (Data = Null) Then ' Data.Length <> DataSize
			Throw New BulkAllocationException<DataBuffer>(Data, DataSize)
		Endif

		Local DataLength:= DataSize ' Data.Length

		' Read from the input-stream, then write to the output-stream:
		InputStream.ReadAll(Data, 0, DataLength)
		OutputStream.WriteAll(Data, 0, DataLength)

		Data.Discard()
	#Else
		'Local Data:Int[DataSize]
		'Local Data:Byte[DataSize]

		' This is terrible, but it's our only option without temporary buffers.
		For Local I:= 1 To DataSize ' Data.Length
			OutputStream.WriteByte(InputStream.ReadByte())
		Next
	#End

	Return
End

Function ResizeBuffer:DataBuffer(Buffer:DataBuffer, Size:Long=AUTOMATIC_LENGTH, CopyData:Bool=True, DiscardOldBuffer:Bool=False, OnlyWhenDifferentSizes:Bool=False)
	Local BufferAvailable:Bool = (Buffer <> Null)

	If (BufferAvailable And OnlyWhenDifferentSizes) Then
		If (Size <> AUTOMATIC_LENGTH And Buffer.Length = Size) Then
			Return Buffer
		Endif
	Endif

	If (Size = AUTOMATIC_LENGTH) Then
		Size = Buffer.Length
	Endif

	' Allocate a new data-buffer.
	Local B:= New DataBuffer(Size)

	' Copy the buffer's bytes over to 'B'.
	If (BufferAvailable) Then
		If (CopyData) Then
			' Copy the contents of 'Buffer' to the newly generated buffer-object.
			Buffer.CopyBytes(0, B, 0, Buffer.Length)
		Endif

		If (DiscardOldBuffer) Then
			' Discard the old buffer.
			Buffer.Discard()
		Endif
	Endif

	' Return the newly generated buffer.
	Return B
End

Function SetBuffer:Void(Output:DataBuffer, Value:Byte, Count:Int, Offset:Int=0)
	For Local I:= Offset Until (Count + Offset)
		Output.PokeByte(I, Value)
	Next

	Return
End

Function SetBuffer:Void(Output:DataBuffer, Value:Byte, Offset:Int=0)
	SetBuffer(Output, Value, Output.Length - Offset, Offset)

	Return
End